From 33fc0a610f83a44e94e5a6751093e34fafd645fd Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 15 Sep 2023 17:18:47 +0100 Subject: [PATCH] Fix deprecated behaviour in timeline drawer 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 # --- qiskit/visualization/timeline/core.py | 45 ++++--------- qiskit/visualization/timeline/generators.py | 65 ++++++++++++++----- ...deprecated-bit-index-7277aa6e2a903cb7.yaml | 13 ++++ .../visualization/timeline/test_core.py | 17 ----- .../visualization/timeline/test_generators.py | 9 ++- 5 files changed, 82 insertions(+), 67 deletions(-) create mode 100644 releasenotes/notes/timeline-visualisation-deprecated-bit-index-7277aa6e2a903cb7.yaml diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 1e9e59a714a3..0b0ae59845ef 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -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 @@ -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) @@ -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 @@ -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 diff --git a/qiskit/visualization/timeline/generators.py b/qiskit/visualization/timeline/generators.py index d786cffac495..f746adb5ce4d 100644 --- a/qiskit/visualization/timeline/generators.py +++ b/qiskit/visualization/timeline/generators.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. @@ -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. @@ -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 = [] @@ -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]: @@ -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: @@ -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. @@ -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, @@ -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. diff --git a/releasenotes/notes/timeline-visualisation-deprecated-bit-index-7277aa6e2a903cb7.yaml b/releasenotes/notes/timeline-visualisation-deprecated-bit-index-7277aa6e2a903cb7.yaml new file mode 100644 index 000000000000..53d09d7787b8 --- /dev/null +++ b/releasenotes/notes/timeline-visualisation-deprecated-bit-index-7277aa6e2a903cb7.yaml @@ -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. diff --git a/test/python/visualization/timeline/test_core.py b/test/python/visualization/timeline/test_core.py index 447618acf0eb..395444dc437c 100644 --- a/test/python/visualization/timeline/test_core.py +++ b/test/python/visualization/timeline/test_core.py @@ -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) diff --git a/test/python/visualization/timeline/test_generators.py b/test/python/visualization/timeline/test_generators.py index cf89741c9bf0..83443b27b9eb 100644 --- a/test/python/visualization/timeline/test_generators.py +++ b/test/python/visualization/timeline/test_generators.py @@ -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 @@ -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])