Skip to content

Commit

Permalink
Fix deprecated behaviour in timeline drawer
Browse files Browse the repository at this point in the history
This removes the year-old deprecation of attempting to use the timeline
drawer to draw an unscheduled circuit.

Internally, it also removes all use of the deprecated `Bit` properties
`Bit.index` and `Bit.register`.  This is achieved by making it possible
for generator functions to accept the complete `QuantumCircuit` program
as a keyword argument, if they advertise that they can handle this by
setting a special `accepts_program` attribute on themselves to `True`.
This somewhat convoluted setup is because the generator functions are
supposed to be arbitrary and user-defininable in custom stylesheets, so
we cannot unilaterally change the call signature.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch fix-timeline-deprecated-bit
# Your branch is up to date with 'ibm/main'.
#
# Changes to be committed:
#	modified:   qiskit/visualization/timeline/core.py
#	modified:   qiskit/visualization/timeline/generators.py
#	new file:   releasenotes/notes/timeline-visualisation-deprecated-bit-index-7277aa6e2a903cb7.yaml
#	modified:   test/python/visualization/timeline/test_core.py
#	modified:   test/python/visualization/timeline/test_generators.py
#
  • Loading branch information
jakelishman committed Sep 15, 2023
1 parent 7d9d32e commit 33fc0a6
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 67 deletions.
45 changes: 13 additions & 32 deletions qiskit/visualization/timeline/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
the lookup table of the handler and the drawings by using this data key.
"""
from __future__ import annotations
import warnings
from collections.abc import Iterator
from copy import deepcopy
from functools import partial
Expand Down Expand Up @@ -150,29 +149,7 @@ def load_program(self, program: circuit.QuantumCircuit):
not_gate_like = (circuit.Barrier,)

if getattr(program, "_op_start_times") is None:
# Run scheduling for backward compatibility
from qiskit import transpile
from qiskit.transpiler import InstructionDurations, TranspilerError

warnings.warn(
"Visualizing un-scheduled circuit with timeline drawer has been deprecated. "
"This circuit should be transpiled with scheduler though it consists of "
"instructions with explicit durations.",
DeprecationWarning,
)

try:
program = transpile(
program,
scheduling_method="alap",
instruction_durations=InstructionDurations(),
optimization_level=0,
)
except TranspilerError as ex:
raise VisualizationError(
f"Input circuit {program.name} is not scheduled and it contains "
"operations with unknown delays. This cannot be visualized."
) from ex
raise VisualizationError(f"Input circuit {program.name} is not scheduled")

for t0, instruction in zip(program.op_start_times, program.data):
bits = list(instruction.qubits) + list(instruction.clbits)
Expand All @@ -187,8 +164,9 @@ def load_program(self, program: circuit.QuantumCircuit):
bit_position=bit_pos,
)
for gen in self.generator["gates"]:
obj_generator = partial(gen, formatter=self.formatter)
for datum in obj_generator(gate_source):
if getattr(gen, "accepts_program", False):
gen = partial(gen, program=program)
for datum in gen(gate_source, formatter=self.formatter):
self.add_data(datum)
if len(bits) > 1 and bit_pos == 0:
# Generate draw object for gate-gate link
Expand All @@ -197,23 +175,26 @@ def load_program(self, program: circuit.QuantumCircuit):
t0=line_pos, opname=instruction.operation.name, bits=bits
)
for gen in self.generator["gate_links"]:
obj_generator = partial(gen, formatter=self.formatter)
for datum in obj_generator(link_source):
if getattr(gen, "accepts_program", False):
gen = partial(gen, program=program)
for datum in gen(link_source, formatter=self.formatter):
self.add_data(datum)
if isinstance(instruction.operation, circuit.Barrier):
# Generate draw object for barrier
barrier_source = types.Barrier(t0=t0, bits=bits, bit_position=bit_pos)
for gen in self.generator["barriers"]:
obj_generator = partial(gen, formatter=self.formatter)
for datum in obj_generator(barrier_source):
if getattr(gen, "accepts_program", False):
gen = partial(gen, program=program)
for datum in gen(barrier_source, formatter=self.formatter):
self.add_data(datum)

self.bits = list(program.qubits) + list(program.clbits)
for bit in self.bits:
for gen in self.generator["bits"]:
# Generate draw objects for bit
obj_generator = partial(gen, formatter=self.formatter)
for datum in obj_generator(bit):
if getattr(gen, "accepts_program", False):
gen = partial(gen, program=program)
for datum in gen(bit, formatter=self.formatter):
self.add_data(datum)

# update time range
Expand Down
65 changes: 49 additions & 16 deletions qiskit/visualization/timeline/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ def my_object_generator(
# your code here: create and return drawings related to the gate object.
```
If a generator object has the attribute ``accepts_program`` set to ``True``, then the generator will
be called with an additional keyword argument ``program: QuantumCircuit``.
2. generator.bits
In this stylesheet entry the input data is `types.Bits` and generates timeline objects
Expand All @@ -53,6 +56,9 @@ def my_object_generator(
# your code here: create and return drawings related to the bit object.
```
If a generator object has the attribute ``accepts_program`` set to ``True``, then the generator will
be called with an additional keyword argument ``program: QuantumCircuit``.
3. generator.barriers
In this stylesheet entry the input data is `types.Barrier` and generates barrier objects
Expand All @@ -69,6 +75,9 @@ def my_object_generator(
# your code here: create and return drawings related to the barrier object.
```
If a generator object has the attribute ``accepts_program`` set to ``True``, then the generator will
be called with an additional keyword argument ``program: QuantumCircuit``.
4. generator.gate_links
In this stylesheet entry the input data is `types.GateLink` and generates barrier objects
Expand All @@ -85,15 +94,18 @@ def my_object_generator(
# your code here: create and return drawings related to the link object.
```
If a generator object has the attribute ``accepts_program`` set to ``True``, then the generator will
be called with an additional keyword argument ``program: QuantumCircuit``.
Arbitrary generator function satisfying the above format can be accepted.
Returned `ElementaryData` can be arbitrary subclasses that are implemented in
the plotter API.
"""

import warnings
from typing import List, Union, Dict, Any, Optional

from typing import List, Union, Dict, Any

from qiskit.circuit import Qubit, QuantumCircuit
from qiskit.circuit.exceptions import CircuitError
from qiskit.visualization.timeline import types, drawings

Expand Down Expand Up @@ -191,7 +203,7 @@ def gen_sched_gate(


def gen_full_gate_name(
gate: types.ScheduledGate, formatter: Dict[str, Any]
gate: types.ScheduledGate, formatter: Dict[str, Any], program: Optional[QuantumCircuit] = None
) -> List[drawings.TextData]:
"""Generate gate name.
Expand All @@ -204,6 +216,7 @@ def gen_full_gate_name(
Args:
gate: Gate information source.
formatter: Dictionary of stylesheet settings.
program: Optional program that the bits are a part of.
Returns:
List of `TextData` drawings.
Expand Down Expand Up @@ -232,12 +245,15 @@ def gen_full_gate_name(
label_latex = rf"{latex_name}"

# bit index
with warnings.catch_warnings():
warnings.simplefilter("ignore")
if len(gate.bits) > 1:
bits_str = ", ".join(map(str, [bit.index for bit in gate.bits]))
label_plain += f"[{bits_str}]"
label_latex += f"[{bits_str}]"
if len(gate.bits) > 1:
if program is None:
# This is horribly hacky and mostly meaningless, but there's no other distinguisher
# available to us if all we have is a `Bit` instance.
bits_str = ", ".join(str(id(bit))[-3:] for bit in gate.bits)
else:
bits_str = ", ".join(f"{program.find_bit(bit).index}" for bit in gate.bits)
label_plain += f"[{bits_str}]"
label_latex += f"[{bits_str}]"

# parameter list
params = []
Expand Down Expand Up @@ -276,6 +292,9 @@ def gen_full_gate_name(
return [drawing]


gen_full_gate_name.accepts_program = True


def gen_short_gate_name(
gate: types.ScheduledGate, formatter: Dict[str, Any]
) -> List[drawings.TextData]:
Expand Down Expand Up @@ -367,7 +386,11 @@ def gen_timeslot(bit: types.Bits, formatter: Dict[str, Any]) -> List[drawings.Bo
return [drawing]


def gen_bit_name(bit: types.Bits, formatter: Dict[str, Any]) -> List[drawings.TextData]:
def gen_bit_name(
bit: types.Bits,
formatter: Dict[str, Any],
program: Optional[QuantumCircuit] = None,
) -> List[drawings.TextData]:
"""Generate bit label.
Stylesheet:
Expand All @@ -376,6 +399,7 @@ def gen_bit_name(bit: types.Bits, formatter: Dict[str, Any]) -> List[drawings.Te
Args:
bit: Bit object associated to this drawing.
formatter: Dictionary of stylesheet settings.
program: Optional program that the bits are a part of.
Returns:
List of `TextData` drawings.
Expand All @@ -388,12 +412,18 @@ def gen_bit_name(bit: types.Bits, formatter: Dict[str, Any]) -> List[drawings.Te
"ha": "right",
}

with warnings.catch_warnings():
warnings.simplefilter("ignore")
label_plain = f"{bit.register.name}"
label_latex = r"{{\rm {register}}}_{{{index}}}".format(
register=bit.register.prefix, index=bit.index
)
if program is None:
warnings.warn("bits cannot be accurately named without passing a 'program'", stacklevel=2)
label_plain = "q" if isinstance(bit, Qubit) else "c"
label_latex = rf"{{\rm {label_plain}}}"
else:
loc = program.find_bit(bit)
if loc.registers:
label_plain = loc.registers[-1][0].name
label_latex = rf"{{\rm {loc.registers[-1][0].prefix}}}_{{{loc.registers[-1][1]}}}"
else:
label_plain = "q" if isinstance(bit, Qubit) else "c"
label_latex = rf"{{\rm {label_plain}}}_{{{loc.index}}}"

drawing = drawings.TextData(
data_type=types.LabelType.BIT_NAME,
Expand All @@ -408,6 +438,9 @@ def gen_bit_name(bit: types.Bits, formatter: Dict[str, Any]) -> List[drawings.Te
return [drawing]


gen_bit_name.accepts_program = True


def gen_barrier(barrier: types.Barrier, formatter: Dict[str, Any]) -> List[drawings.LineData]:
"""Generate barrier line.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
features:
- |
When defining a custom stylesheet for the pulse timeline drawer :func:`qiskit.visualization.timeline_drawer`,
"generator" functions that have the object attribute ``accepts_program`` set to ``True`` will
receive an extra keyword argument ``program`` containing the full scheduled
:class:`.QuantumCircuit` being drawn.
upgrade:
- |
Using the timeline drawer :func:`qiskit.visualization.timeline_drawer` with an unscheduled
circuit will now raise a :exc:`.VisualizationError`, rather than silently attempting to schedule
with no known instruction durations. This behaviour had been deprecated since Qiskit 0.37 in
June 2022.
17 changes: 0 additions & 17 deletions test/python/visualization/timeline/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,23 +130,6 @@ def test_object_outside_xlimit(self):

self.assertEqual(len(drawings_tested), 12)

def test_non_transpiled_delay_circuit(self):
"""Test non-transpiled circuit containing instruction which is trivial on duration."""
circ = QuantumCircuit(1)
circ.delay(10, 0)

canvas = core.DrawerCanvas(stylesheet=self.style)
canvas.generator = {
"gates": [generators.gen_sched_gate],
"bits": [],
"barriers": [],
"gate_links": [],
}

with self.assertWarns(DeprecationWarning):
canvas.load_program(circ)
self.assertEqual(len(canvas._collections), 1)

def test_multi_measurement_with_clbit_not_shown(self):
"""Test generating bit link drawings of measurements when clbits is disabled."""
circ = QuantumCircuit(2, 2)
Expand Down
9 changes: 7 additions & 2 deletions test/python/visualization/timeline/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ def setUp(self) -> None:
"""Setup."""
super().setUp()

self.qubit = list(qiskit.QuantumRegister(1, "bar"))[0]
self.program = qiskit.QuantumCircuit(qiskit.QuantumRegister(1, "bar"))
self.program._op_start_times = []
self.qubit = self.program.qubits[0]

style = stylesheet.QiskitTimelineStyle()
self.formatter = style.formatter
Expand Down Expand Up @@ -238,7 +240,10 @@ def test_gen_timeslot(self):

def test_gen_bit_name(self):
"""Test gen_bit_name generator."""
drawing_obj = generators.gen_bit_name(self.qubit, self.formatter)[0]
with self.assertWarnsRegex(UserWarning, "bits cannot be accurately named"):
generators.gen_bit_name(self.qubit, self.formatter)

drawing_obj = generators.gen_bit_name(self.qubit, self.formatter, program=self.program)[0]

self.assertEqual(drawing_obj.data_type, str(types.LabelType.BIT_NAME.value))
self.assertListEqual(list(drawing_obj.xvals), [types.AbstractCoordinate.LEFT])
Expand Down

0 comments on commit 33fc0a6

Please sign in to comment.