Skip to content

Commit

Permalink
Fix deprecated behaviour in timeline drawer
Browse files Browse the repository at this point in the history
Internally, it 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-definable in custom stylesheets, so
we cannot unilaterally change the call signature.
  • Loading branch information
jakelishman committed Sep 16, 2023
1 parent 7d9d32e commit 55b774b
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 26 deletions.
20 changes: 12 additions & 8 deletions qiskit/visualization/timeline/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,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 +198,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,7 @@
---
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.
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 55b774b

Please sign in to comment.