From a8ddcdf8c58898594c04cefffb4fd49d01e8452a Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Apr 2022 03:22:49 +0900 Subject: [PATCH 01/10] Cleanup timeline and add schedule info to scheduled circuit. --- qiskit/transpiler/basepasses.py | 9 + qiskit/transpiler/runningpassmanager.py | 10 + qiskit/visualization/timeline/core.py | 72 +++--- qiskit/visualization/timeline/events.py | 121 --------- qiskit/visualization/timeline/interface.py | 5 + ...anup-timeline-drawer-a6287bdab4459e6e.yaml | 27 ++ .../visualization/timeline/test_events.py | 234 ------------------ 7 files changed, 90 insertions(+), 388 deletions(-) delete mode 100644 qiskit/visualization/timeline/events.py create mode 100644 releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml delete mode 100644 test/python/visualization/timeline/test_events.py diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index 832b79e148a5..5beab817cde5 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -136,6 +136,15 @@ def __call__(self, circuit, property_set=None): result_circuit._clbit_write_latency = self.property_set["clbit_write_latency"] if self.property_set["conditional_latency"] is not None: result_circuit._conditional_latency = self.property_set["conditional_latency"] + if self.property_set["node_start_time"]: + # This is dictionary keyed on the DAGOpNode, which is invalidated once + # dag is converted into circuit. So this schedule information is + # also converted into list with the same ordering with circuit.data. + topological_start_times = [] + start_times = self.property_set["node_start_time"] + for dag_node in result.topological_op_nodes(): + topological_start_times.append(start_times[dag_node]) + result_circuit._node_start_time = self.property_set["node_start_time"] return result_circuit diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index 43af938d1879..ad4a0b25d338 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -132,6 +132,16 @@ def run(self, circuit, output_name=None, callback=None): circuit._clbit_write_latency = self.property_set["clbit_write_latency"] circuit._conditional_latency = self.property_set["conditional_latency"] + if self.property_set["node_start_time"]: + # This is dictionary keyed on the DAGOpNode, which is invalidated once + # dag is converted into circuit. So this schedule information is + # also converted into list with the same ordering with circuit.data. + topological_start_times = [] + start_times = self.property_set["node_start_time"] + for dag_node in dag.topological_op_nodes(): + topological_start_times.append(start_times[dag_node]) + circuit._node_start_time = topological_start_times + return circuit def _do_pass(self, pass_, dag, options): diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 3fd6ae9e473b..d9f8a2b583c9 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -51,7 +51,6 @@ from copy import deepcopy from functools import partial -from itertools import chain from typing import Tuple, Iterator, Dict from enum import Enum @@ -59,7 +58,7 @@ from qiskit import circuit from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.timeline import drawings, events, types +from qiskit.visualization.timeline import drawings, types from qiskit.visualization.timeline.stylesheet import QiskitTimelineStyle @@ -143,43 +142,50 @@ def load_program(self, program: circuit.QuantumCircuit): Args: program: Scheduled circuit object to draw. """ - self.bits = program.qubits + program.clbits - stop_time = 0 + not_gate_like = (circuit.Barrier,) + + for t0, (inst, qargs, cargs) in zip(getattr(program, "_node_start_time"), program.data): + bits = qargs + cargs + for bit_pos, bit in enumerate(qargs + cargs): + if not isinstance(inst, not_gate_like): + # Generate draw object for gates + gate_source = types.ScheduledGate( + t0=t0, + operand=inst, + duration=inst.duration, + bits=bits, + bit_position=bit_pos, + ) + for gen in self.generator["gates"]: + obj_generator = partial(gen, formatter=self.formatter) + for datum in obj_generator(gate_source): + self.add_data(datum) + if len(bits) > 1 and bit_pos == 0: + # Generate draw object for gate-gate link + line_pos = t0 + 0.5 * inst.duration + link_source = types.GateLink(t0=line_pos, opname=inst.name, bits=bits) + for gen in self.generator["gate_links"]: + obj_generator = partial(gen, formatter=self.formatter) + for datum in obj_generator(link_source): + self.add_data(datum) + if isinstance(inst, 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): + self.add_data(datum) + self.bits = program.qubits + program.clbits for bit in self.bits: - bit_events = events.BitEvents.load_program(scheduled_circuit=program, bit=bit) - - # create objects associated with gates - for gen in self.generator["gates"]: - obj_generator = partial(gen, formatter=self.formatter) - draw_targets = [obj_generator(gate) for gate in bit_events.get_gates()] - for data in list(chain.from_iterable(draw_targets)): - self.add_data(data) - - # create objects associated with gate links - for gen in self.generator["gate_links"]: - obj_generator = partial(gen, formatter=self.formatter) - draw_targets = [obj_generator(link) for link in bit_events.get_gate_links()] - for data in list(chain.from_iterable(draw_targets)): - self.add_data(data) - - # create objects associated with barrier - for gen in self.generator["barriers"]: - obj_generator = partial(gen, formatter=self.formatter) - draw_targets = [obj_generator(barrier) for barrier in bit_events.get_barriers()] - for data in list(chain.from_iterable(draw_targets)): - self.add_data(data) - - # create objects associated with bit for gen in self.generator["bits"]: + # Generate draw objects for bit obj_generator = partial(gen, formatter=self.formatter) - for data in obj_generator(bit): - self.add_data(data) - - stop_time = max(stop_time, bit_events.stop_time) + for datum in obj_generator(bit): + self.add_data(datum) # update time range - t_end = max(stop_time, self.formatter["margin.minimum_duration"]) + t_end = max(program.duration, self.formatter["margin.minimum_duration"]) self.set_time_range(t_start=0, t_end=t_end) def set_time_range(self, t_start: int, t_end: int): diff --git a/qiskit/visualization/timeline/events.py b/qiskit/visualization/timeline/events.py deleted file mode 100644 index 51374ac4c938..000000000000 --- a/qiskit/visualization/timeline/events.py +++ /dev/null @@ -1,121 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Bit event manager for scheduled circuits. - -This module provides a :py:class:`BitEvents` class that manages a series of instructions for a -specific circuit bit. Bit-wise filtering of the circuit program makes the arrangement of bits -easier in the core drawer function. The `BitEvents` class is expected to be called -by other programs (not by end-users). - -The :py:class:`BitEvents` class instance is created with the class method ``load_program``: - ```python - event = BitEvents.load_program(sched_circuit, qregs[0]) - ``` - -Loaded circuit instructions are saved as ``ScheduledGate``, which is a collection of instruction, -associated time, and bits. All gate instructions are returned by the `.get_gates` method. -Instruction types specified in `BitEvents._non_gates` are not considered as gates. -If an instruction is associated with multiple bits and the target bit of the class instance is -the primary bit of the instruction, the instance also generates a ``GateLink`` object -that shows the relationship between bits during multi-bit gates. -""" -from typing import List, Iterator - -from qiskit import circuit -from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.timeline import types - - -class BitEvents: - """Bit event table.""" - - _non_gates = (circuit.Barrier,) - - def __init__(self, bit: types.Bits, instructions: List[types.ScheduledGate], t_stop: int): - """Create new event for the specified bit. - - Args: - bit: Bit object associated with this event table. - instructions: List of scheduled gate object. - t_stop: Stop time of this bit. - """ - self.bit = bit - self.instructions = instructions - self.stop_time = t_stop - - @classmethod - def load_program(cls, scheduled_circuit: circuit.QuantumCircuit, bit: types.Bits): - """Build new BitEvents from scheduled circuit. - - Args: - scheduled_circuit: Scheduled circuit object to draw. - bit: Target bit object. - - Returns: - BitEvents: New `BitEvents` object. - - Raises: - VisualizationError: When the circuit is not transpiled with duration. - """ - t0 = 0 - tf = scheduled_circuit.qubit_stop_time(bit) - - instructions = [] - for inst, qargs, cargs in scheduled_circuit.data: - associated_bits = qargs + cargs - if bit not in associated_bits: - continue - - duration = inst.duration - if duration is None: - raise VisualizationError( - "Instruction {oper} has no duration. " - "You need to transpile the QuantumCircuit with " - "gate durations before drawing.".format(oper=inst) - ) - - instructions.append( - types.ScheduledGate( - t0=t0, - operand=inst, - duration=duration, - bits=associated_bits, - bit_position=associated_bits.index(bit), - ) - ) - t0 += duration - - return BitEvents(bit, instructions, tf) - - def get_gates(self) -> Iterator[types.ScheduledGate]: - """Return scheduled gates.""" - for inst in self.instructions: - if not isinstance(inst.operand, self._non_gates): - yield inst - - def get_barriers(self) -> Iterator[types.Barrier]: - """Return barriers.""" - for inst in self.instructions: - if isinstance(inst.operand, circuit.Barrier): - barrier = types.Barrier(t0=inst.t0, bits=inst.bits, bit_position=inst.bit_position) - yield barrier - - def get_gate_links(self) -> Iterator[types.GateLink]: - """Return link between multi-bit gates.""" - for inst in self.get_gates(): - # generate link iff this is the primary bit. - if len(inst.bits) > 1 and inst.bit_position == 0: - t0 = inst.t0 + 0.5 * inst.duration - link = types.GateLink(t0=t0, opname=inst.operand.name, bits=inst.bits) - yield link diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py index 689757fe5f1e..2b6c0dc923e7 100644 --- a/qiskit/visualization/timeline/interface.py +++ b/qiskit/visualization/timeline/interface.py @@ -346,6 +346,11 @@ def draw( This feature enables you to control the most of appearance of the output image without modifying the codebase of the scheduled circuit drawer. """ + if not hasattr(program, "_node_start_time"): + raise VisualizationError( + f"Input circuit {program.name} is not scheduled. Run one of scheduling analysis passes." + ) + # update stylesheet temp_style = stylesheet.QiskitTimelineStyle() temp_style.update(style or stylesheet.IQXStandard()) diff --git a/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml b/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml new file mode 100644 index 000000000000..e12e6f20b07c --- /dev/null +++ b/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml @@ -0,0 +1,27 @@ +--- +upgrade: + - | + A protected attribute :attr:`_node_start_time` is silently added to + :class:`~QuantumCircuit` when one of scheduling analysis passes is run on the circuit. + This information can be used to obtain circuit instruction with instruction time. + + .. code-block:: python + + from qiskit import QuantumCircuit, transpile + from qiskit.test.mock import FakeMontreal + + backend = FakeMontreal() + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + + qct = transpile( + qc, backend, initial_layout=[0, 1], coupling_map=[[0, 1]], scheduling_method="alap" + ) + scheduled_insts = list(zip(qct._node_start_time, qct.data)) + +fixes: + - | + Time misalignment bug of drawing classical register with :func:`~timeline_drawer` + has been fixed. Now classical register slots are drawn at correct position. diff --git a/test/python/visualization/timeline/test_events.py b/test/python/visualization/timeline/test_events.py deleted file mode 100644 index 994c1d539fc5..000000000000 --- a/test/python/visualization/timeline/test_events.py +++ /dev/null @@ -1,234 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Tests for event of timeline drawer.""" - -import qiskit -from qiskit import QuantumCircuit, transpile -from qiskit.circuit import library -from qiskit.test import QiskitTestCase -from qiskit.visualization.timeline import events, types - - -class TestLoadScheduledCircuit(QiskitTestCase): - """Test for loading program.""" - - def setUp(self) -> None: - """Setup.""" - super().setUp() - - circ = QuantumCircuit(3) - circ.delay(100, 2) - circ.barrier(0, 1, 2) - circ.h(0) - circ.cx(0, 1) - - self.circ = transpile( - circ, - scheduling_method="alap", - basis_gates=["h", "cx"], - instruction_durations=[("h", 0, 200), ("cx", [0, 1], 1000)], - optimization_level=0, - ) - - def test_create_from_program(self): - """Test factory method.""" - bit_event_q0 = events.BitEvents.load_program(self.circ, self.circ.qregs[0][0]) - bit_event_q1 = events.BitEvents.load_program(self.circ, self.circ.qregs[0][1]) - bit_event_q2 = events.BitEvents.load_program(self.circ, self.circ.qregs[0][2]) - - gates_q0 = list(bit_event_q0.get_gates()) - links_q0 = list(bit_event_q0.get_gate_links()) - barriers_q0 = list(bit_event_q0.get_barriers()) - - self.assertEqual(len(gates_q0), 3) - self.assertEqual(len(links_q0), 1) - self.assertEqual(len(barriers_q0), 1) - - # h gate - self.assertEqual(gates_q0[1].t0, 100) - - # cx gate - self.assertEqual(gates_q0[2].t0, 300) - - # link - self.assertEqual(links_q0[0].t0, 800) - - # barrier - self.assertEqual(barriers_q0[0].t0, 100) - - gates_q1 = list(bit_event_q1.get_gates()) - links_q1 = list(bit_event_q1.get_gate_links()) - barriers_q1 = list(bit_event_q1.get_barriers()) - - self.assertEqual(len(gates_q1), 3) - self.assertEqual(len(links_q1), 0) - self.assertEqual(len(barriers_q1), 1) - - # cx gate - self.assertEqual(gates_q0[2].t0, 300) - - # barrier - self.assertEqual(barriers_q1[0].t0, 100) - - gates_q2 = list(bit_event_q2.get_gates()) - links_q2 = list(bit_event_q2.get_gate_links()) - barriers_q2 = list(bit_event_q2.get_barriers()) - - self.assertEqual(len(gates_q2), 2) - self.assertEqual(len(links_q2), 0) - self.assertEqual(len(barriers_q2), 1) - - # barrier - self.assertEqual(barriers_q2[0].t0, 100) - - -class TestBitEvents(QiskitTestCase): - """Tests for bit events.""" - - def setUp(self) -> None: - """Setup.""" - super().setUp() - - self.qubits = list(qiskit.QuantumRegister(2)) - self.clbits = list(qiskit.ClassicalRegister(2)) - - self.instructions = [ - types.ScheduledGate( - t0=0, operand=library.U1Gate(0), duration=0, bits=[self.qubits[0]], bit_position=0 - ), - types.ScheduledGate( - t0=0, - operand=library.U2Gate(0, 0), - duration=10, - bits=[self.qubits[0]], - bit_position=0, - ), - types.ScheduledGate( - t0=10, - operand=library.CXGate(), - duration=50, - bits=[self.qubits[0], self.qubits[1]], - bit_position=0, - ), - types.ScheduledGate( - t0=100, - operand=library.U3Gate(0, 0, 0), - duration=20, - bits=[self.qubits[0]], - bit_position=0, - ), - types.ScheduledGate( - t0=120, - operand=library.Barrier(2), - duration=0, - bits=[self.qubits[0], self.qubits[1]], - bit_position=0, - ), - types.ScheduledGate( - t0=120, - operand=library.CXGate(), - duration=50, - bits=[self.qubits[1], self.qubits[0]], - bit_position=1, - ), - types.ScheduledGate( - t0=200, - operand=library.Barrier(1), - duration=0, - bits=[self.qubits[0]], - bit_position=0, - ), - types.ScheduledGate( - t0=200, - operand=library.Measure(), - duration=100, - bits=[self.qubits[0], self.clbits[0]], - bit_position=0, - ), - ] - - def test_gate_output(self): - """Test gate output.""" - bit_event = events.BitEvents(self.qubits[0], self.instructions, 300) - - gates = list(bit_event.get_gates()) - ref_list = [ - types.ScheduledGate( - t0=0, operand=library.U1Gate(0), duration=0, bits=[self.qubits[0]], bit_position=0 - ), - types.ScheduledGate( - t0=0, - operand=library.U2Gate(0, 0), - duration=10, - bits=[self.qubits[0]], - bit_position=0, - ), - types.ScheduledGate( - t0=10, - operand=library.CXGate(), - duration=50, - bits=[self.qubits[0], self.qubits[1]], - bit_position=0, - ), - types.ScheduledGate( - t0=100, - operand=library.U3Gate(0, 0, 0), - duration=20, - bits=[self.qubits[0]], - bit_position=0, - ), - types.ScheduledGate( - t0=120, - operand=library.CXGate(), - duration=50, - bits=[self.qubits[1], self.qubits[0]], - bit_position=1, - ), - types.ScheduledGate( - t0=200, - operand=library.Measure(), - duration=100, - bits=[self.qubits[0], self.clbits[0]], - bit_position=0, - ), - ] - - self.assertListEqual(gates, ref_list) - - def test_barrier_output(self): - """Test barrier output.""" - bit_event = events.BitEvents(self.qubits[0], self.instructions, 200) - - barriers = list(bit_event.get_barriers()) - ref_list = [ - types.Barrier(t0=120, bits=[self.qubits[0], self.qubits[1]], bit_position=0), - types.Barrier(t0=200, bits=[self.qubits[0]], bit_position=0), - ] - - self.assertListEqual(barriers, ref_list) - - def test_bit_link_output(self): - """Test link output.""" - bit_event = events.BitEvents(self.qubits[0], self.instructions, 250) - - links = list(bit_event.get_gate_links()) - ref_list = [ - types.GateLink( - t0=35.0, opname=library.CXGate().name, bits=[self.qubits[0], self.qubits[1]] - ), - types.GateLink( - t0=250.0, opname=library.Measure().name, bits=[self.qubits[0], self.clbits[0]] - ), - ] - - self.assertListEqual(links, ref_list) From ff377ab10a894afacc02c47ec570c3fc1b288391 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Apr 2022 04:14:56 +0900 Subject: [PATCH 02/10] review comment --- qiskit/circuit/quantumcircuit.py | 18 ++++++++++++++++++ qiskit/transpiler/basepasses.py | 2 +- qiskit/transpiler/runningpassmanager.py | 2 +- qiskit/visualization/timeline/core.py | 2 +- qiskit/visualization/timeline/interface.py | 5 ----- ...eanup-timeline-drawer-a6287bdab4459e6e.yaml | 10 +++++----- 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7951277be2e0..4a2640be3f36 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -245,6 +245,7 @@ def __init__( # Data contains a list of instructions and their contexts, # in the order they were applied. self._data = [] + self._op_start_times = [] # A stack to hold the instruction sets that are being built up during for-, if- and # while-block construction. These are stored as a stripped down sequence of instructions, @@ -299,6 +300,23 @@ def data(self) -> QuantumCircuitData: """ return QuantumCircuitData(self) + @property + def op_start_times(self) -> List[int]: + """Return a list of operation start times. + + This attribute is enabled once one of scheduling analysis passes + runs on the quantum circuit. + + Returns: + List of integers representing instruction start times. + The index corresponds to the index of instruction in :attr:`QuantumCircuit.data`. + """ + if not self._op_start_times: + raise AttributeError( + "This circuit is not scheduled. Run one of scheduling passes." + ) + return self._op_start_times + @data.setter def data( self, data_input: List[Tuple[Instruction, List[QubitSpecifier], List[ClbitSpecifier]]] diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index 5beab817cde5..a45a119d0274 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -144,7 +144,7 @@ def __call__(self, circuit, property_set=None): start_times = self.property_set["node_start_time"] for dag_node in result.topological_op_nodes(): topological_start_times.append(start_times[dag_node]) - result_circuit._node_start_time = self.property_set["node_start_time"] + result_circuit._op_start_times = self.property_set["node_start_time"] return result_circuit diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index ad4a0b25d338..6cff16cb2c03 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -140,7 +140,7 @@ def run(self, circuit, output_name=None, callback=None): start_times = self.property_set["node_start_time"] for dag_node in dag.topological_op_nodes(): topological_start_times.append(start_times[dag_node]) - circuit._node_start_time = topological_start_times + circuit._op_start_times = topological_start_times return circuit diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index d9f8a2b583c9..114b448d1603 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -144,7 +144,7 @@ def load_program(self, program: circuit.QuantumCircuit): """ not_gate_like = (circuit.Barrier,) - for t0, (inst, qargs, cargs) in zip(getattr(program, "_node_start_time"), program.data): + for t0, (inst, qargs, cargs) in zip(program.op_start_times, program.data): bits = qargs + cargs for bit_pos, bit in enumerate(qargs + cargs): if not isinstance(inst, not_gate_like): diff --git a/qiskit/visualization/timeline/interface.py b/qiskit/visualization/timeline/interface.py index 2b6c0dc923e7..689757fe5f1e 100644 --- a/qiskit/visualization/timeline/interface.py +++ b/qiskit/visualization/timeline/interface.py @@ -346,11 +346,6 @@ def draw( This feature enables you to control the most of appearance of the output image without modifying the codebase of the scheduled circuit drawer. """ - if not hasattr(program, "_node_start_time"): - raise VisualizationError( - f"Input circuit {program.name} is not scheduled. Run one of scheduling analysis passes." - ) - # update stylesheet temp_style = stylesheet.QiskitTimelineStyle() temp_style.update(style or stylesheet.IQXStandard()) diff --git a/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml b/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml index e12e6f20b07c..94b76ed5e56e 100644 --- a/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml +++ b/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml @@ -1,9 +1,9 @@ --- -upgrade: +features: - | - A protected attribute :attr:`_node_start_time` is silently added to - :class:`~QuantumCircuit` when one of scheduling analysis passes is run on the circuit. - This information can be used to obtain circuit instruction with instruction time. + New attribute :attr:`op_start_times` has been added to :class:`~QuantumCircuit`. + This information is populated when one of scheduling analysis passes is run on the circuit. + It can be used to obtain circuit instruction with instruction time, for example: .. code-block:: python @@ -19,7 +19,7 @@ upgrade: qct = transpile( qc, backend, initial_layout=[0, 1], coupling_map=[[0, 1]], scheduling_method="alap" ) - scheduled_insts = list(zip(qct._node_start_time, qct.data)) + scheduled_insts = list(zip(qct.op_start_times, qct.data)) fixes: - | From febd47f1d4ac9ed7dd75ec7d55d91dc0432c5cf6 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Apr 2022 04:51:41 +0900 Subject: [PATCH 03/10] fix bug --- qiskit/visualization/timeline/core.py | 42 +++++++++++++++---- .../visualization/timeline/test_core.py | 3 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 114b448d1603..917bb4c14fe8 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -48,7 +48,7 @@ If a plotter provides object handler for plotted shapes, the plotter API can manage the lookup table of the handler and the drawings by using this data key. """ - +import warnings from copy import deepcopy from functools import partial from typing import Tuple, Iterator, Dict @@ -144,6 +144,30 @@ def load_program(self, program: circuit.QuantumCircuit): """ not_gate_like = (circuit.Barrier,) + if not getattr(program, "_op_start_times"): + # 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() + ) + except TranspilerError: + raise VisualizationError( + f"Input circuit {program.name} is not scheduled and it contains " + "operations with unknown delays. This cannot be visualized." + ) + for t0, (inst, qargs, cargs) in zip(program.op_start_times, program.data): bits = qargs + cargs for bit_pos, bit in enumerate(qargs + cargs): @@ -160,14 +184,14 @@ def load_program(self, program: circuit.QuantumCircuit): obj_generator = partial(gen, formatter=self.formatter) for datum in obj_generator(gate_source): self.add_data(datum) - if len(bits) > 1 and bit_pos == 0: - # Generate draw object for gate-gate link - line_pos = t0 + 0.5 * inst.duration - link_source = types.GateLink(t0=line_pos, opname=inst.name, bits=bits) - for gen in self.generator["gate_links"]: - obj_generator = partial(gen, formatter=self.formatter) - for datum in obj_generator(link_source): - self.add_data(datum) + if len(bits) > 1 and bit_pos == 0: + # Generate draw object for gate-gate link + line_pos = t0 + 0.5 * inst.duration + link_source = types.GateLink(t0=line_pos, opname=inst.name, bits=bits) + for gen in self.generator["gate_links"]: + obj_generator = partial(gen, formatter=self.formatter) + for datum in obj_generator(link_source): + self.add_data(datum) if isinstance(inst, circuit.Barrier): # Generate draw object for barrier barrier_source = types.Barrier(t0=t0, bits=bits, bit_position=bit_pos) diff --git a/test/python/visualization/timeline/test_core.py b/test/python/visualization/timeline/test_core.py index 38bf60c39df7..447618acf0eb 100644 --- a/test/python/visualization/timeline/test_core.py +++ b/test/python/visualization/timeline/test_core.py @@ -143,7 +143,8 @@ def test_non_transpiled_delay_circuit(self): "gate_links": [], } - canvas.load_program(circ) + with self.assertWarns(DeprecationWarning): + canvas.load_program(circ) self.assertEqual(len(canvas._collections), 1) def test_multi_measurement_with_clbit_not_shown(self): From 0e3e7020f1c19807246bc095a3d440588927205e Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Apr 2022 05:01:15 +0900 Subject: [PATCH 04/10] review comments Co-authored-by: Matthew Treinish --- qiskit/circuit/quantumcircuit.py | 10 +++++++--- qiskit/visualization/timeline/core.py | 2 +- test/python/circuit/test_circuit_properties.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4a2640be3f36..62f924a5bb73 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -245,7 +245,7 @@ def __init__( # Data contains a list of instructions and their contexts, # in the order they were applied. self._data = [] - self._op_start_times = [] + self._op_start_times = None # A stack to hold the instruction sets that are being built up during for-, if- and # while-block construction. These are stored as a stripped down sequence of instructions, @@ -310,10 +310,14 @@ def op_start_times(self) -> List[int]: Returns: List of integers representing instruction start times. The index corresponds to the index of instruction in :attr:`QuantumCircuit.data`. + + Raises: + AttributeError: When circuit is not scheduled. """ - if not self._op_start_times: + if self._op_start_times is None: raise AttributeError( - "This circuit is not scheduled. Run one of scheduling passes." + "This circuit is not scheduled. " + "To schedule it run the circuit through one of the transpiler scheduling passes." ) return self._op_start_times diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 917bb4c14fe8..fdc31fc07a25 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -144,7 +144,7 @@ def load_program(self, program: circuit.QuantumCircuit): """ not_gate_like = (circuit.Barrier,) - if not getattr(program, "_op_start_times"): + if getattr(program, "_op_start_times") is None: # Run scheduling for backward compatibility from qiskit import transpile from qiskit.transpiler import InstructionDurations, TranspilerError diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index c4355f2eee05..83f83a7b0ab2 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -1266,6 +1266,16 @@ def test_metadata_copy_does_not_share_state(self): self.assertEqual(qc1.metadata["a"], 0) + def test_scheduling(self): + """Test cannot return schedule information without scheduling.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + + with self.assertRaises(AttributeError): + # pylint: disable=pointless-statement + qc.op_start_times + if __name__ == "__main__": unittest.main() From 98f29e16d2f110f0e0380abb58283f383bbf00cd Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 14 Apr 2022 05:03:54 +0900 Subject: [PATCH 05/10] lint --- qiskit/visualization/timeline/core.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index fdc31fc07a25..06b8a676b583 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -141,6 +141,9 @@ def load_program(self, program: circuit.QuantumCircuit): Args: program: Scheduled circuit object to draw. + + Raises: + VisualizationError: When circuit is not scheduled. """ not_gate_like = (circuit.Barrier,) @@ -158,15 +161,13 @@ def load_program(self, program: circuit.QuantumCircuit): try: program = transpile( - program, - scheduling_method="alap", - instruction_durations=InstructionDurations() + program, scheduling_method="alap", instruction_durations=InstructionDurations() ) - except TranspilerError: + 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 for t0, (inst, qargs, cargs) in zip(program.op_start_times, program.data): bits = qargs + cargs From bab723a2fb6d9ef8b8b7c2d9dccd14fea16218af Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 19 Apr 2022 04:17:37 +0900 Subject: [PATCH 06/10] disable cyclic import lint check (likely a bug) --- qiskit/visualization/timeline/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 06b8a676b583..1c095a03f313 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -136,6 +136,7 @@ def add_data(self, data: drawings.ElementaryData): data.bits = [b for b in data.bits if not isinstance(b, circuit.Clbit)] self._collections[data.data_key] = data + # pylint: disable=cyclic-import def load_program(self, program: circuit.QuantumCircuit): """Load quantum circuit and create drawing.. From 1fa5382df47e6dd778d791b270f1cbf9639ce300 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 19 Apr 2022 05:36:28 +0900 Subject: [PATCH 07/10] fix import path --- qiskit/visualization/timeline/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 1c095a03f313..30e1b9e87e83 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -136,7 +136,6 @@ def add_data(self, data: drawings.ElementaryData): data.bits = [b for b in data.bits if not isinstance(b, circuit.Clbit)] self._collections[data.data_key] = data - # pylint: disable=cyclic-import def load_program(self, program: circuit.QuantumCircuit): """Load quantum circuit and create drawing.. @@ -150,7 +149,7 @@ def load_program(self, program: circuit.QuantumCircuit): if getattr(program, "_op_start_times") is None: # Run scheduling for backward compatibility - from qiskit import transpile + from qiskit.compiler import transpile from qiskit.transpiler import InstructionDurations, TranspilerError warnings.warn( From 8bd4550e624baf1d736a98036a535704b44994c4 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 19 Apr 2022 14:07:21 +0900 Subject: [PATCH 08/10] Revert "fix import path" This reverts commit 1fa5382df47e6dd778d791b270f1cbf9639ce300. --- qiskit/visualization/timeline/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py index 30e1b9e87e83..1c095a03f313 100644 --- a/qiskit/visualization/timeline/core.py +++ b/qiskit/visualization/timeline/core.py @@ -136,6 +136,7 @@ def add_data(self, data: drawings.ElementaryData): data.bits = [b for b in data.bits if not isinstance(b, circuit.Clbit)] self._collections[data.data_key] = data + # pylint: disable=cyclic-import def load_program(self, program: circuit.QuantumCircuit): """Load quantum circuit and create drawing.. @@ -149,7 +150,7 @@ def load_program(self, program: circuit.QuantumCircuit): if getattr(program, "_op_start_times") is None: # Run scheduling for backward compatibility - from qiskit.compiler import transpile + from qiskit import transpile from qiskit.transpiler import InstructionDurations, TranspilerError warnings.warn( From 7d663858f89892742dd1087d6fe77e00d8fc71fe Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 20 Apr 2022 00:02:03 +0900 Subject: [PATCH 09/10] fix bug --- qiskit/transpiler/basepasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index a45a119d0274..5e006d16c8da 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -144,7 +144,7 @@ def __call__(self, circuit, property_set=None): start_times = self.property_set["node_start_time"] for dag_node in result.topological_op_nodes(): topological_start_times.append(start_times[dag_node]) - result_circuit._op_start_times = self.property_set["node_start_time"] + result_circuit._op_start_times = topological_start_times return result_circuit From 8891220d691110471dc5bb145b03e6487cc81b82 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 20 Apr 2022 03:57:21 +0900 Subject: [PATCH 10/10] add deprecation for unscheduled circuit drawing --- .../notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml b/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml index 94b76ed5e56e..8d7de2fec700 100644 --- a/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml +++ b/releasenotes/notes/cleanup-timeline-drawer-a6287bdab4459e6e.yaml @@ -25,3 +25,9 @@ fixes: - | Time misalignment bug of drawing classical register with :func:`~timeline_drawer` has been fixed. Now classical register slots are drawn at correct position. +deprecations: + - | + Calling :func:`~timeline_drawer` with unscheduled circuit has been deprecated. + All circuits, e.g. even though one consisting only of delay instructions, + must be transpiled with ``scheduling_method`` option to generate + schedule information being stored in :attr:`QuantumCircuit.op_start_times`.