Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method to compute estimated duration of scheduled circuit (backport #13783) #13881

Merged
merged 5 commits into from
Feb 20, 2025
Merged
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
112 changes: 112 additions & 0 deletions crates/accelerate/src/circuit_duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2025
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use pyo3::intern;
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use qiskit_circuit::dag_circuit::{DAGCircuit, NodeType, Wire};
use qiskit_circuit::operations::{Operation, OperationRef, Param};

use crate::nlayout::PhysicalQubit;
use crate::target_transpiler::Target;
use crate::QiskitError;
use rustworkx_core::dag_algo::longest_path;
use rustworkx_core::petgraph::stable_graph::StableDiGraph;
use rustworkx_core::petgraph::visit::{EdgeRef, IntoEdgeReferences};

/// Estimate the duration of a scheduled circuit in seconds
#[pyfunction]
pub(crate) fn compute_estimated_duration(dag: &DAGCircuit, target: &Target) -> PyResult<f64> {
let dt = target.dt;

let get_duration =
|edge: <&StableDiGraph<NodeType, Wire> as IntoEdgeReferences>::EdgeRef| -> PyResult<f64> {
let node_weight = &dag.dag()[edge.target()];
match node_weight {
NodeType::Operation(inst) => {
let name = inst.op.name();
let qubits = dag.get_qargs(inst.qubits);
let physical_qubits: Vec<PhysicalQubit> =
qubits.iter().map(|x| PhysicalQubit::new(x.0)).collect();

if name == "delay" {
let dur = &inst.params.as_ref().unwrap()[0];
let OperationRef::Instruction(op) = inst.op.view() else {
unreachable!("Invalid type for delay instruction");
};
Python::with_gil(|py| {
let unit: String = op
.instruction
.getattr(py, intern!(py, "unit"))?
.extract(py)?;
if unit == "dt" {
if let Some(dt) = dt {
match dur {
Param::Float(val) => Ok(val * dt),
Param::Obj(val) => {
let dur_float: f64 = val.extract(py)?;
Ok(dur_float * dt)
},
Param::ParameterExpression(_) => Err(QiskitError::new_err(
"Circuit contains parameterized delays, can't compute a duration estimate with this circuit"
)),
}
} else {
Err(QiskitError::new_err(
"Circuit contains delays in dt but the target doesn't specify dt"
))
}
} else if unit == "s" {
match dur {
Param::Float(val) => Ok(*val),
_ => Err(QiskitError::new_err(
"Invalid type for parameter value for delay in circuit",
)),
}
} else {
Err(QiskitError::new_err(
"Circuit contains delays in units other then seconds or dt, the circuit is not scheduled."
))
}
})
} else if name == "barrier" {
Ok(0.)
} else {
match target.get_duration(name, &physical_qubits) {
Some(dur) => Ok(dur),
None => Err(QiskitError::new_err(format!(
"Duration not found for {} on qubits: {:?}",
name, qubits
))),
}
}
}
NodeType::QubitOut(_) | NodeType::ClbitOut(_) => Ok(0.),
NodeType::ClbitIn(_) | NodeType::QubitIn(_) => {
Err(QiskitError::new_err("Invalid circuit provided"))
}
_ => Err(QiskitError::new_err(
"Circuit contains Vars, duration can't be calculated with classical variables",
)),
}
};
match longest_path(dag.dag(), get_duration)? {
Some((_, weight)) => Ok(weight),
None => Err(QiskitError::new_err("Invalid circuit provided")),
}
}

pub fn compute_duration(m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(compute_estimated_duration))?;
Ok(())
}
1 change: 1 addition & 0 deletions crates/accelerate/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use pyo3::import_exception;
pub mod barrier_before_final_measurement;
pub mod basis;
pub mod check_map;
pub mod circuit_duration;
pub mod circuit_library;
pub mod commutation_analysis;
pub mod commutation_cancellation;
Expand Down
11 changes: 11 additions & 0 deletions crates/accelerate/src/target_transpiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,17 @@ impl Target {
})
}

/// Get the duration of a given instruction in the target
pub fn get_duration(&self, name: &str, qargs: &[PhysicalQubit]) -> Option<f64> {
self.gate_map.get(name).and_then(|gate_props| {
let qargs_key: Qargs = qargs.iter().cloned().collect();
match gate_props.get(Some(&qargs_key)) {
Some(props) => props.as_ref().and_then(|inst_props| inst_props.duration),
None => None,
}
})
}

/// Get an iterator over the indices of all physical qubits of the target
pub fn physical_qubits(&self) -> impl ExactSizeIterator<Item = usize> {
0..self.num_qubits.unwrap_or_default()
Expand Down
1 change: 1 addition & 0 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
add_submodule(m, ::qiskit_accelerate::barrier_before_final_measurement::barrier_before_final_measurements_mod, "barrier_before_final_measurement")?;
add_submodule(m, ::qiskit_accelerate::basis::basis, "basis")?;
add_submodule(m, ::qiskit_accelerate::check_map::check_map_mod, "check_map")?;
add_submodule(m, ::qiskit_accelerate::circuit_duration::compute_duration, "circuit_duration")?;
add_submodule(m, ::qiskit_accelerate::circuit_library::circuit_library, "circuit_library")?;
add_submodule(m, ::qiskit_accelerate::commutation_analysis::commutation_analysis, "commutation_analysis")?;
add_submodule(m, ::qiskit_accelerate::commutation_cancellation::commutation_cancellation, "commutation_cancellation")?;
Expand Down
1 change: 1 addition & 0 deletions qiskit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
sys.modules["qiskit._accelerate.twirling"] = _accelerate.twirling
sys.modules["qiskit._accelerate.high_level_synthesis"] = _accelerate.high_level_synthesis
sys.modules["qiskit._accelerate.remove_identity_equiv"] = _accelerate.remove_identity_equiv
sys.modules["qiskit._accelerate.circuit_duration"] = _accelerate.circuit_duration

from qiskit.exceptions import QiskitError, MissingOptionalLibraryError

Expand Down
65 changes: 64 additions & 1 deletion qiskit/circuit/quantumcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import numpy as np
from qiskit._accelerate.circuit import CircuitData
from qiskit._accelerate.circuit import StandardGate
from qiskit._accelerate.circuit_duration import compute_estimated_duration
from qiskit.exceptions import QiskitError
from qiskit.utils.multiprocessing import is_main_process
from qiskit.circuit.instruction import Instruction
Expand Down Expand Up @@ -156,6 +157,8 @@ class QuantumCircuit:
:attr:`data` List of individual :class:`CircuitInstruction`\\ s that make up the
circuit.
:attr:`duration` Total duration of the circuit, added by scheduling transpiler passes.
This attribute is deprecated and :meth:`.estimate_duration` should
be used instead.

:attr:`layout` Hardware layout and routing information added by the transpiler.
:attr:`num_ancillas` The number of ancilla qubits in the circuit.
Expand Down Expand Up @@ -921,8 +924,9 @@ class QuantumCircuit:

If a :class:`QuantumCircuit` has been scheduled as part of a transpilation pipeline, the timing
information for individual qubits can be accessed. The whole-circuit timing information is
available through the :attr:`duration`, :attr:`unit` and :attr:`op_start_times` attributes.
available through the :meth:`estimate_duration` method and :attr:`op_start_times` attribute.

.. automethod:: estimate_duration
.. automethod:: qubit_duration
.. automethod:: qubit_start_time
.. automethod:: qubit_stop_time
Expand Down Expand Up @@ -6654,6 +6658,65 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float:

return 0 # If there are no instructions over bits

def estimate_duration(self, target, unit: str = "s") -> int | float:
"""Estimate the duration of a scheduled circuit

This method computes the estimate of the circuit duration by finding
the longest duration path in the circuit based on the durations
provided by a given target. This method only works for simple circuits
that have no control flow or other classical feed-forward operations.

Args:
target (Target): The :class:`.Target` instance that contains durations for
the instructions if the target is missing duration data for any of the
instructions in the circuit an :class:`.QiskitError` will be raised. This
should be the same target object used as the target for transpilation.
unit: The unit to return the duration in. This defaults to "s" for seconds
but this can be a supported SI prefix for seconds returns. For example
setting this to "n" will return in unit of nanoseconds. Supported values
of this type are "f", "p", "n", "u", "µ", "m", "k", "M", "G", "T", and
"P". Additionally, a value of "dt" is also accepted to output an integer
in units of "dt". For this to function "dt" must be specified in the
``target``.

Returns:
The estimated duration for the execution of a single shot of the circuit in
the specified unit.

Raises:
QiskitError: If the circuit is not scheduled or contains other
details that prevent computing an estimated duration from
(such as parameterized delay).
"""
from qiskit.converters import circuit_to_dag

dur = compute_estimated_duration(circuit_to_dag(self), target)
if unit == "s":
return dur
if unit == "dt":
from qiskit.circuit.duration import duration_in_dt # pylint: disable=cyclic-import

return duration_in_dt(dur, target.dt)

prefix_dict = {
"f": 1e-15,
"p": 1e-12,
"n": 1e-9,
"u": 1e-6,
"µ": 1e-6,
"m": 1e-3,
"k": 1e3,
"M": 1e6,
"G": 1e9,
"T": 1e12,
"P": 1e15,
}
if unit not in prefix_dict:
raise QiskitError(
f"Specified unit: {unit} is not a valid/supported SI prefix, 's', or 'dt'"
)
return dur / prefix_dict[unit]


class _OuterCircuitScopeInterface(CircuitScopeInterface):
# This is an explicit interface-fulfilling object friend of QuantumCircuit that acts as its
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
features_circuits:
- |
Added a new method, :meth:`.QuantumCircuit.estimate_duration`, to compute
the estimated duration of a scheduled circuit output from the :mod:`.transpiler`.
This should be used if you need an estimate of the full circuit duration instead
of the deprecated :attr:`.QuantumCircuit.duration` attribute.
Loading