Skip to content

Commit c0e7431

Browse files
committed
Improve deprecation of unit and duration
The unit and duration attributes of QuantumCircuit and Instruction were introduced in Qiskit#13224 however the way the PR went about this was a bit to abbreviated and it had several downsides. The most noticable was that it introduced a significant runtime regression as the rust internals were accessing the deprecated getter. This is fixed so that the rust code doesn't try to access any deprecated attributes. However, more importantly there were several edge cases not accoutned for in the PR. Mainly, DAGCircuit's matching attribute were not deprecated, but then also specific use cases of the attributes were not addressed. The primary driver of the per instruction duration and unit were the timeline visualization which didn't have a method to function without these data attributes. This commit addresses this by adding a new target argument that is used to provide a backend's target which is the source of truth for durations in a post unit/duration `Instruction`. The other notable edge case is the `Delay` instruction which will need to retain the duration and unit attributes as the fields are integral for that instruction. The `Delay` is updated to handle them explicitly outside of the `Instrution` data model to avoid complications around the deprecation.
1 parent 9778462 commit c0e7431

11 files changed

Lines changed: 185 additions & 49 deletions

File tree

crates/circuit/src/circuit_instruction.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,15 +637,23 @@ impl<'py> FromPyObject<'py> for OperationFromPython {
637637
let extract_extra = || -> PyResult<_> {
638638
let unit = {
639639
// We accept Python-space `None` or `"dt"` as both meaning the default `"dt"`.
640-
let raw_unit = ob.getattr(intern!(py, "unit"))?;
640+
let raw_unit = ob.getattr(intern!(py, "_unit"))?;
641641
(!(raw_unit.is_none()
642642
|| raw_unit.eq(ExtraInstructionAttributes::default_unit(py))?))
643643
.then(|| raw_unit.extract::<String>())
644644
.transpose()?
645645
};
646+
// Delay uses the `duration` attr as an alias for param[0] which isn't deprecated
647+
// while all other instructions have deprecated `duration` so we want to access
648+
// the inner _duration to avoid the deprecation warning's run time overhead.
649+
let duration = if ob.getattr(intern!(py, "name"))?.extract::<String>()? != "delay" {
650+
ob.getattr(intern!(py, "_duration"))?.extract()?
651+
} else {
652+
ob.getattr(intern!(py, "duration"))?.extract()?
653+
};
646654
Ok(ExtraInstructionAttributes::new(
647655
ob.getattr(intern!(py, "label"))?.extract()?,
648-
ob.getattr(intern!(py, "duration"))?.extract()?,
656+
duration,
649657
unit,
650658
ob.getattr(intern!(py, "condition"))?.extract()?,
651659
))

crates/circuit/src/dag_circuit.rs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ use hashbrown::{HashMap, HashSet};
3535
use indexmap::IndexMap;
3636
use itertools::Itertools;
3737

38-
use pyo3::exceptions::{PyIndexError, PyRuntimeError, PyTypeError, PyValueError};
38+
use pyo3::exceptions::{
39+
PyDeprecationWarning, PyIndexError, PyRuntimeError, PyTypeError, PyValueError,
40+
};
3941
use pyo3::intern;
4042
use pyo3::prelude::*;
43+
4144
use pyo3::types::{
4245
IntoPyDict, PyDict, PyInt, PyIterator, PyList, PySequence, PySet, PyString, PyTuple, PyType,
4346
};
@@ -257,10 +260,10 @@ pub struct DAGCircuit {
257260
/// Global phase.
258261
global_phase: Param,
259262
/// Duration.
260-
#[pyo3(get, set)]
263+
#[pyo3(set)]
261264
duration: Option<PyObject>,
262265
/// Unit of duration.
263-
#[pyo3(get, set)]
266+
#[pyo3(set)]
264267
unit: String,
265268

266269
// Note: these are tracked separately from `qubits` and `clbits`
@@ -455,6 +458,45 @@ impl DAGCircuit {
455458
})
456459
}
457460

461+
/// The total duration of the circuit, set by a scheduling transpiler pass. Its unit is
462+
/// specified by :attr:`.unit`
463+
///
464+
/// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0
465+
#[getter]
466+
fn get_duration(&self, py: Python) -> PyResult<Option<Py<PyAny>>> {
467+
imports::WARNINGS_WARN.get_bound(py).call1((
468+
intern!(
469+
py,
470+
concat!(
471+
"The property ``qiskit.dagcircuit.dagcircuit.DAGCircuit.duration`` is ",
472+
"deprecated as of qiskit 1.3.0. It will be removed in Qiskit 2.0.0.",
473+
)
474+
),
475+
py.get_type_bound::<PyDeprecationWarning>(),
476+
2,
477+
))?;
478+
Ok(self.duration.as_ref().map(|x| x.clone_ref(py)))
479+
}
480+
481+
/// The unit that duration is specified in.
482+
///
483+
/// DEPRECATED since Qiskit 1.3.0 and will be removed in Qiskit 2.0.0
484+
#[getter]
485+
fn get_unit(&self, py: Python) -> PyResult<String> {
486+
imports::WARNINGS_WARN.get_bound(py).call1((
487+
intern!(
488+
py,
489+
concat!(
490+
"The property ``qiskit.dagcircuit.dagcircuit.DAGCircuit.unit`` is ",
491+
"deprecated as of qiskit 1.3.0. It will be removed in Qiskit 2.0.0.",
492+
)
493+
),
494+
py.get_type_bound::<PyDeprecationWarning>(),
495+
2,
496+
))?;
497+
Ok(self.unit.clone())
498+
}
499+
458500
#[getter]
459501
fn input_map(&self, py: Python) -> PyResult<Py<PyDict>> {
460502
let out_dict = PyDict::new_bound(py);

crates/circuit/src/operations.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -450,17 +450,29 @@ impl StandardGate {
450450
extra_attrs.condition(),
451451
);
452452
if label.is_some() || unit.is_some() || duration.is_some() || condition.is_some() {
453-
let kwargs = [
454-
("label", label.to_object(py)),
455-
("unit", extra_attrs.py_unit(py).into_any()),
456-
("duration", duration.to_object(py)),
457-
]
458-
.into_py_dict_bound(py);
453+
let kwargs = [("label", label.to_object(py))].into_py_dict_bound(py);
459454
let mut out = gate_class.call_bound(py, args, Some(&kwargs))?;
455+
let mut mutable = false;
460456
if let Some(condition) = condition {
461-
out = out.call_method0(py, "to_mutable")?;
457+
if !mutable {
458+
out = out.call_method0(py, "to_mutable")?;
459+
mutable = true;
460+
}
462461
out.setattr(py, "condition", condition)?;
463462
}
463+
if let Some(duration) = duration {
464+
if !mutable {
465+
out = out.call_method0(py, "to_mutable")?;
466+
mutable = true;
467+
}
468+
out.setattr(py, "_duration", duration)?;
469+
}
470+
if let Some(unit) = unit {
471+
if !mutable {
472+
out = out.call_method0(py, "to_mutable")?;
473+
}
474+
out.setattr(py, "_unit", unit)?;
475+
}
464476
Ok(out)
465477
} else {
466478
gate_class.call_bound(py, args, None)

qiskit/circuit/delay.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ def __init__(self, duration, unit="dt"):
3333
"""
3434
if unit not in {"s", "ms", "us", "ns", "ps", "dt"}:
3535
raise CircuitError(f"Unknown unit {unit} is specified.")
36-
37-
super().__init__("delay", 1, 0, params=[duration], unit=unit)
36+
# Double underscore to differentiate from the private attribute in
37+
# `Instruction`. This can be changed to `_unit` in 2.0 after we
38+
# remove `unit` and `duration` from the standard instruction model
39+
# as it only will exist in `Delay` after that point.
40+
self.__unit = unit
41+
super().__init__("delay", 1, 0, params=[duration])
3842

3943
broadcast_arguments = Gate.broadcast_arguments
4044

@@ -45,6 +49,17 @@ def inverse(self, annotated: bool = False):
4549
def c_if(self, classical, val):
4650
raise CircuitError("Conditional Delay is not yet implemented.")
4751

52+
@property
53+
def unit(self):
54+
55+
return self.__unit
56+
57+
@unit.setter
58+
def unit(self, value):
59+
if value not in {"s", "ms", "us", "ns", "ps", "dt"}:
60+
raise CircuitError(f"Unknown unit {value} is specified.")
61+
self.__unit = value
62+
4863
@property
4964
def duration(self):
5065
"""Get the duration of this delay."""
@@ -81,7 +96,7 @@ def validate_parameter(self, parameter):
8196
raise CircuitError(
8297
f"Duration for Delay instruction must be positive. Found {parameter}"
8398
)
84-
if self.unit == "dt":
99+
if self.__unit == "dt":
85100
parameter_int = int(parameter)
86101
if parameter != parameter_int:
87102
raise CircuitError("Integer duration is expected for 'dt' unit.")

qiskit/circuit/instruction.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,21 @@ def __init__(self, name, num_qubits, num_clbits, params, duration=None, unit="dt
103103
# list of instructions (and their contexts) that this instruction is composed of
104104
# empty definition means opaque or fundamental instruction
105105
self._definition = None
106+
if duration is not None:
107+
warnings.warn(
108+
"Setting a custom duration per instruction is deprecated as of qiskit "
109+
"1.3.0. It will be removed in Qiskit 2.0.0.",
110+
DeprecationWarning,
111+
stacklevel=2,
112+
)
106113
self._duration = duration
114+
if unit is not None and unit != "dt":
115+
warnings.warn(
116+
"Setting a custom duration per instruction is deprecated as of qiskit "
117+
"1.3.0. It will be removed in Qiskit 2.0.0.",
118+
DeprecationWarning,
119+
stacklevel=2,
120+
)
107121
self._unit = unit
108122

109123
self.params = params # must be at last (other properties may be required for validation)
@@ -347,9 +361,9 @@ def duration(self):
347361
return self._duration
348362

349363
@duration.setter
350-
def duration(self, duration):
364+
def duration(self, value):
351365
"""Set the duration."""
352-
self._duration = duration
366+
self._duration = value
353367

354368
@property
355369
@deprecate_func(since="1.3.0", removal_timeline="in Qiskit 2.0.0", is_property=True)
@@ -358,9 +372,9 @@ def unit(self):
358372
return self._unit
359373

360374
@unit.setter
361-
def unit(self, unit):
375+
def unit(self, value):
362376
"""Set the time unit of duration."""
363-
self._unit = unit
377+
self._unit = value
364378

365379
@deprecate_func(
366380
since="1.2",

qiskit/converters/circuit_to_dag.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord
7373

7474
dagcircuit = core_circuit_to_dag(circuit, copy_operations, qubit_order, clbit_order)
7575

76-
dagcircuit.duration = circuit.duration
77-
dagcircuit.unit = circuit.unit
76+
dagcircuit.duration = circuit._duration
77+
dagcircuit.unit = circuit._unit
7878
return dagcircuit

qiskit/converters/dag_to_circuit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,6 @@ def dag_to_circuit(dag, copy_operations=True):
7474

7575
circuit._data = circuit_data
7676

77-
circuit.duration = dag.duration
78-
circuit.unit = dag.unit
77+
circuit._duration = dag.duration
78+
circuit._unit = dag.unit
7979
return circuit

qiskit/visualization/timeline/core.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import numpy as np
5959

6060
from qiskit import circuit
61+
from qiskit.transpiler.target import Target
6162
from qiskit.visualization.exceptions import VisualizationError
6263
from qiskit.visualization.timeline import drawings, types
6364
from qiskit.visualization.timeline.stylesheet import QiskitTimelineStyle
@@ -138,11 +139,13 @@ def add_data(self, data: drawings.ElementaryData):
138139
self._collections[data.data_key] = data
139140

140141
# pylint: disable=cyclic-import
141-
def load_program(self, program: circuit.QuantumCircuit):
142+
def load_program(self, program: circuit.QuantumCircuit, target: Target | None = None):
142143
"""Load quantum circuit and create drawing..
143144
144145
Args:
145146
program: Scheduled circuit object to draw.
147+
target: The target for the backend the circuit is scheduled for
148+
this contains the instruction durations.
146149
147150
Raises:
148151
VisualizationError: When circuit is not scheduled.
@@ -180,10 +183,30 @@ def load_program(self, program: circuit.QuantumCircuit):
180183
for bit_pos, bit in enumerate(bits):
181184
if not isinstance(instruction.operation, not_gate_like):
182185
# Generate draw object for gates
186+
if target is not None:
187+
duration = None
188+
op_props = target.get(instruction.operation.name)
189+
if op_props is not None:
190+
inst_props = op_props.get(tuple(instruction.qubits))
191+
if inst_props is not None:
192+
duration = inst_props.getattr("duration")
193+
194+
if duration is None:
195+
# Warn here because an incomplete target isn't obvious most of the time
196+
warnings.warn(
197+
"Target doesn't contain a duration for "
198+
f"{instruction.operation.name} on {bit_pos}, this will error in "
199+
"Qiskit 2.0.",
200+
DeprecationWarning,
201+
stacklevel=3,
202+
)
203+
duration = instruction.operation.duration
204+
else:
205+
duration = instruction.operation.duration
183206
gate_source = types.ScheduledGate(
184207
t0=t0,
185208
operand=instruction.operation,
186-
duration=instruction.operation.duration,
209+
duration=duration,
187210
bits=bits,
188211
bit_position=bit_pos,
189212
)

qiskit/visualization/timeline/interface.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
"""
2121

2222
from typing import Optional, Dict, Any, List, Tuple
23+
import warnings
2324

2425
from qiskit import circuit
26+
from qiskit.transpiler.target import Target
2527
from qiskit.exceptions import MissingOptionalLibraryError
2628
from qiskit.visualization.exceptions import VisualizationError
2729
from qiskit.visualization.timeline import types, core, stylesheet
@@ -43,6 +45,7 @@ def draw(
4345
plotter: Optional[str] = types.Plotter.MPL.value,
4446
axis: Optional[Any] = None,
4547
filename: Optional[str] = None,
48+
target: Optional[Target] = None,
4649
*,
4750
show_idle: Optional[bool] = None,
4851
show_barriers: Optional[bool] = None,
@@ -81,6 +84,7 @@ def draw(
8184
the plotters uses given `axis` instead of internally initializing a figure object.
8285
This object format depends on the plotter. See plotters section for details.
8386
filename: If provided the output image is dumped into a file under the filename.
87+
target: The target for the backend the timeline is being generated for.
8488
show_idle: DEPRECATED.
8589
show_barriers: DEPRECATED.
8690
@@ -361,6 +365,14 @@ def draw(
361365
temp_style = stylesheet.QiskitTimelineStyle()
362366
temp_style.update(style or stylesheet.IQXStandard())
363367

368+
if target is None:
369+
warnings.warn(
370+
"Target is not specified in Qiskit 2.0.0 this will be required to get the duration of "
371+
"instructions.",
372+
PendingDeprecationWarning,
373+
stacklevel=2,
374+
)
375+
364376
# update control properties
365377
if idle_wires is not None:
366378
temp_style["formatter.control.show_idle"] = idle_wires
Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
---
2+
features_visualization:
3+
- |
4+
The :func:`.timeline_drawer` visualization function has a new argument
5+
``target``, used to specify a :class:`.Target` object for the
6+
visualization. By default the function using the
7+
:attr:`.Instruction.duration` to get the duration of a given isntruction,
8+
but specifying the target will leverage the timing details inside the
9+
target instead.
210
deprecations_circuits:
311
- |
412
The :attr:`.QuantumCircuit.unit` and :attr:`.QuantumCircuit.duration`
@@ -10,16 +18,28 @@ deprecations_circuits:
1018
guess based on the longest path on the sum of the duration of
1119
:class:`.DAGCircuit` and wouldn't ever correctly account for control flow
1220
or conditionals in the circuit.
21+
- |
22+
The :attr:`.DAGCircuit.unit` and :attr:`.DAGCircuit.duration`
23+
attributes have been deprecated and will be removed in Qiskit 2.0.0. These
24+
attributes were used to track the estimated duration and unit of that
25+
duration to execute on the circuit. However, the values of these attributes
26+
were always limited, as they would only be properly populated if the
27+
transpiler were run with the correct settings. The duration was also only a
28+
guess based on the longest path on the sum of the duration of
29+
:class:`.DAGCircuit` and wouldn't ever correctly account for control flow
30+
or conditionals in the circuit.
1331
- |
1432
The :attr:`.Instruction.duration` and :attr:`.Instruction.unit` attributes
15-
have been deprecated and will be removed in Qiskit 2.0.0. These attributes
16-
were used to attach a custom execution duration and unit for that duration
17-
to an individual instruction. However, the source of truth of the duration
18-
of a gate is the :class:`.BackendV2` :class:`.Target` which contains
19-
the duration for each instruction supported on the backend. The duration of
20-
an instruction is not something that's typically user adjustable and is
21-
an immutable property of the backend. If you were previously using this
22-
capability to experiment with different durations for gates you can
23-
mutate the :attr:`.InstructionProperties.duration` field in a given
24-
:class:`.Target` to set a custom duration for an instruction on a backend
25-
(the unit is always in seconds in the :class:`.Target`).
33+
have been deprecated and will be removed in Qiskit 2.0.0. This includes
34+
setting the ``unit`` or ``duration`` arguments for any :class:`.Instruction`
35+
or subclass. These attributes were used to attach a custom execution
36+
duration and unit for that duration to an individual instruction. However,
37+
the source of truth of the duration of a gate is the :class:`.BackendV2`
38+
:class:`.Target` which contains the duration for each instruction supported
39+
on the backend. The duration of an instruction is not something that's
40+
typically user adjustable and is an immutable property of the backend. If
41+
you were previously using this capability to experiment with different
42+
durations for gates you can mutate the
43+
:attr:`.InstructionProperties.duration` field in a given :class:`.Target` to
44+
set a custom duration for an instruction on a backend (the unit is always in
45+
seconds in the :class:`.Target`).

0 commit comments

Comments
 (0)