Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update plot_gate_map() family to leverage graphviz for visualization #10208

Merged
merged 119 commits into from
Sep 22, 2023

Conversation

maxwell04-wq
Copy link
Contributor

Summary

Fixes #9031

Details and comments

  • Updated the following modules in qiskit-terra/qiskit/visualization/gate_map.py to leverage graphviz plotting for gate maps
    • plot_gate_map
    • plot_coupling_map
    • plot_circuit_layout
    • plot_error_map
  • Uses both rx and matplotlib for plot_error_map, uses only rx.graphvix_draw for all other functions.
  • ⚠️ The test_gate_map.py file has been updated accordingly. However, the reference images will need to be updated as the tests are failing for maps with larger qubits and no backends (expected behavior as per Update plot_gate_map (and related visualization functions) to leverage rustworkx.visualization.graphviz_draw for unknown coupling maps #9031)
  • qiskit.utils.optionals does not have a HAS_RUSTWORKX check. This can be added to ensure that rustworkx is installed prior to running the gate_map.py modules.
  • The graphviz-draw() function does not provide the choice to draw directed or undirected graphs. I have left the plot_directed parameter in the functions, but as of now it does not affect graph plots.
  • Since planar_layout() is not yet available in rustwokx, the graphs have been plotted using spring_layout(). This can be modified in future versions.

Update the gate_map.py to migrate the visualization modules from matplotlib to rustworkx.graphviz
Updated tests for the modified gate_map.py file
@maxwell04-wq maxwell04-wq requested review from a team and nonhermitian as code owners June 6, 2023 07:21
@qiskit-bot qiskit-bot added the Community PR PRs from contributors that are not 'members' of the Qiskit repo label Jun 6, 2023
@qiskit-bot
Copy link
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

One or more of the the following people are requested to review this:

@mtreinish
Copy link
Member

* `qiskit.utils.optionals` does not have a `HAS_RUSTWORKX` check. This can be added to ensure that rustworkx is installed prior to running the `gate_map.py` modules.

Rustworkx is a requirement for qiskit, so it will always be installed when using qiskit. You will need a an optional check on HAS_GRAPHVIZ though because that will be required to run the function. rustworkx.graphviz_draw() has it's own check for this but the exception returned is in a different format so using the qiskit decorator is probably best.

* The `graphviz-draw()` function does not provide the choice to draw directed or undirected graphs. I have left the `plot_directed` parameter in the functions, but as of now it does not affect graph plots.

The easiest way to implement this is to use PyDiGraph.to_undirected() (https://qiskit.org/documentation/retworkx/apiref/rustworkx.PyDiGraph.to_undirected.html ) to create a PyGraph object from a PyDiGraph. Then if you pass that to graphviz_draw() it'll be an undirected graph visualization.

* Since `planar_layout()` is not yet available in rustwokx, the graphs have been plotted using `spring_layout()`. This can be modified in future versions.

TBH, the output of planar layout isn't really suitable for this use case. At the time I added that comment to the code I assumed it was without actually looking (based solely on the name). But this isn't an issue if you're doing a matplotlib based drawing the only real option is to use spring_layout() if you're using rustworkx's layout function.

The thing which might be more interesting to try (and definitely higher quality) instead of rustworkx's spring_layout is to leverage graphviz as a layout engine. You can use either the dot or json output format's from graphviz and then parse that output for the pos parameter on each node which you then can feed to the matplotlib drawer (likely after a coordinate system translation).

@maxwell04-wq
Copy link
Contributor Author

@mtreinish I have a question about the tests: I have modified the checks, but need to update the images with which the graphs are compared. Should I wait for a review before changing the figures for the tests?

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start, thanks for looking at this. I just did a quick high level pass to start and left some comments inline.

qiskit/visualization/gate_map.py Outdated Show resolved Hide resolved
qiskit/visualization/gate_map.py Outdated Show resolved Hide resolved
qiskit/visualization/gate_map.py Outdated Show resolved Hide resolved
qiskit/visualization/gate_map.py Outdated Show resolved Hide resolved
qiskit/visualization/gate_map.py Outdated Show resolved Hide resolved
qiskit/visualization/gate_map.py Outdated Show resolved Hide resolved
@mtreinish
Copy link
Member

@mtreinish I have a question about the tests: I have modified the checks, but need to update the images with which the graphs are compared. Should I wait for a review before changing the figures for the tests?

You can go ahead an update them now, in the worst case if we change the formatting of the output visualizations at all we can easily just regenerate them again to account for any future changes.

@maxwell04-wq
Copy link
Contributor Author

maxwell04-wq commented Jun 11, 2023

@mtreinish thank you for the thorough review of my PR. I have modified my code to reuse the exisiting code. What really helped was using the mpl_draw function of rustworx instead of graphviz_draw. I was also able to define the graph layout using qubit_coordinates, and now the rendered figures are much more similar to the original ones, (albeit rotated at a 90º angle).

One issue that may need to be addressed is using a better function than spring_layout for graph layouts. A grid_layout function can be developed for rustworkx, much like networkx. Should I open an issue for it? As of now, in the test file, if we increase the similarity tolerance of images in the test_plot_circuit_layout function from 0.1 to 0.2 (like the rest of the test functions), the tests will pass.

@nonhermitian, as this issue is a part of UnitaryHack, can we try to have the PR merged by June 13? 😄 I realise that it's a close call, but I'll be available at your discretion.

Test file updated so that all tests can be passed.
@mtreinish
Copy link
Member

@mtreinish thank you for the thorough review of my PR. I have modified my code to reuse the exisiting code. What really helped was using the mpl_draw function of rustworx instead of graphviz_draw. I was also able to define the graph layout using qubit_coordinates, and now the rendered figures are much more similar to the original ones, (albeit rotated at a 90º angle).

One issue that may need to be addressed is using a better function than spring_layout for graph layouts. A grid_layout function can be developed for rustworkx, much like networkx. Should I open an issue for it? As of now, in the test file, if we increase the similarity tolerance of images in the test_plot_circuit_layout function from 0.1 to 0.2 (like the rest of the test functions), the tests will pass.

This is the core issue of #9031 that the algorithmic layout functions in rustworkx don't scale for large graphs. This is why I used graphviz_draw in the issue as graphviz will be able to layout the graph effectively no matter how large it is. I wasn't too concerned by the format of the visualization changing slightly as a result of using graphviz_draw. But if you'd prefer to use mpl_draw or alternatively the legacy manual mpl code what I'd recommend is doing what I described the other day is to use graphviz solely as a layout engine (#10208 (comment) ). Then we can leave the rendering to mpl and take advantage of graphviz's better layout.

My concern with just using mpl_draw though is rx.spring_layout (or any of the other layout methods available in rustworkx) doesn't scale. We do have an issue to track adding new layout methods: Qiskit/rustworkx#438 so if you're interested we can add a grid layout (I can also help you get started working on the implementation if you're interested in it). But, by itself having a grid layout method in rustworkx isn't really a general solution for #9031 because it assumes a grid like lattice connectivity. For example, if you had a 1000 qubit ring layout a grid layout wouldn't be a good layout.

I think the best way to test this issue is to build a couple of large qubit count fake backends and test them to see what the graphs look like. For example using main if you run:

import numpy as np
import rustworkx as rx

from qiskit import transpile, QuantumCircuit
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target, InstructionProperties
from qiskit.circuit.library import XGate, SXGate, RZGate, CZGate
from qiskit.circuit import Measure, Delay, Parameter, IfElseOp

from qiskit.visualization import plot_gate_map

class FakeMultiChip(BackendV2):
    """Fake multi chip backend."""

    def __init__(self, distance=3, number_of_chips=3):
        """Instantiate a new fake multi chip backend.

        Args:
            distance (int): The heavy hex code distance to use for each chips'
                coupling map. This number **must** be odd. The distance relates
                to the number of qubits by:
                :math:`n = \\frac{5d^2 - 2d - 1}{2}` where :math:`n` is the
                number of qubits and :math:`d` is the ``distance``
            number_of_chips (int): The number of chips to have in the multichip backend
                each chip will be a heavy hex graph of ``distance`` code distance.
        """
        super().__init__(name='multi_chip')
        graph = rx.generators.directed_heavy_hex_graph(distance, bidirectional=False)
        num_qubits = len(graph) * number_of_chips
        rng = np.random.default_rng(seed=12345678942)
        rz_props = {}
        x_props = {}
        sx_props = {}
        measure_props = {}
        delay_props = {}
        self._target = Target("Fake multi-chip backend", num_qubits=num_qubits)
        for i in range(num_qubits):
            qarg = (i,)
            rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
            x_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            sx_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            measure_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-3, 1e-1), duration=rng.uniform(1e-8, 9e-7)
            )
            delay_props[qarg] = None
        self._target.add_instruction(XGate(), x_props)
        self._target.add_instruction(SXGate(), sx_props)
        self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
        self._target.add_instruction(Measure(), measure_props)
        self._target.add_instruction(Delay(Parameter("t")), delay_props)
        cz_props = {}
        for i in range(number_of_chips):
            for root_edge in graph.edge_list():
                offset = i * len(graph)
                edge = (root_edge[0] + offset, root_edge[1] + offset)
                cz_props[edge] = InstructionProperties(
                    error=rng.uniform(1e-5, 5e-3), duration=rng.uniform(1e-8, 9e-7)
                )
        self._target.add_instruction(CZGate(), cz_props)
        self._target.add_instruction(IfElseOp, name="if_else")

    @property
    def target(self):
        return self._target

    @property
    def max_circuits(self):
        return None

    @classmethod
    def _default_options(cls):
        return Options(shots=1024)

    def run(self, circuit, **kwargs):
        raise NotImplementedError("Lasciate ogne speranza, voi ch'intrate")


class FakeGiantRing(BackendV2):
    """Fake multi chip backend."""

    def __init__(self):
        """Instantiate a new 1k qubit ring backend."""
        super().__init__(name='multi_chip')
        graph = rx.generators.directed_cycle_graph(1000)
        num_qubits = len(graph)
        rng = np.random.default_rng(seed=12345678942)
        rz_props = {}
        x_props = {}
        sx_props = {}
        measure_props = {}
        delay_props = {}
        self._target = Target("Fake multi-chip backend", num_qubits=num_qubits)
        for i in range(num_qubits):
            qarg = (i,)
            rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
            x_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            sx_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            measure_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-3, 1e-1), duration=rng.uniform(1e-8, 9e-7)
            )
            delay_props[qarg] = None
        self._target.add_instruction(XGate(), x_props)
        self._target.add_instruction(SXGate(), sx_props)
        self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
        self._target.add_instruction(Measure(), measure_props)
        self._target.add_instruction(Delay(Parameter("t")), delay_props)
        cz_props = {}
        for root_edge in graph.edge_list():
            edge = (root_edge[0], root_edge[1])
            cz_props[edge] = InstructionProperties(
                error=rng.uniform(1e-5, 5e-3), duration=rng.uniform(1e-8, 9e-7)
            )
        self._target.add_instruction(CZGate(), cz_props)
        self._target.add_instruction(IfElseOp, name="if_else")

    @property
    def target(self):
        return self._target

    @property
    def max_circuits(self):
        return None

    @classmethod
    def _default_options(cls):
        return Options(shots=1024)

    def run(self, circuit, **kwargs):
        raise NotImplementedError("Lasciate ogne speranza, voi ch'intrate")


if __name__ == "__main__":
    backend_multichip = FakeMultiChip(15, 5)
    backend_ring = FakeGiantRing()
    plot_gate_map(backend_multichip).savefig('giant_multichip.png')
    plot_gate_map(backend_ring).savefig('ring.png')

this is what you get out today:

giant_multichip.png:
giant_multichip

ring.png:
ring

which is less than useful :) While this output is with the current main branch, I expect this PR's branch using mpl_draw() to have similar output because it's relying on the same layout function under the covers. That script might be helpful in testing any potential solution to see how the proposed change performs on larger scale backends.

@mtreinish
Copy link
Member

Out of curiosity I pulled the PR branch locally and ran the script I posted above and this was the output:

giant_multichip.png:
giant_multichip_with_pr

ring.png:
ring_with_pr

which is better than the layout from the manual mpl code before, but it really shows what I was talking about with the rustworkx spring_layout function via mpl_draw() not being noticeably different.

@mtreinish
Copy link
Member

Finally showing the output from graphviz using the basic script I put in #9031:

giant_multichip.png:

giant_multichip_graphviz

ring.png:

ring_graphviz

@maxwell04-wq
Copy link
Contributor Author

In that case,

@mtreinish thank you for the thorough review of my PR. I have modified my code to reuse the exisiting code. What really helped was using the mpl_draw function of rustworx instead of graphviz_draw. I was also able to define the graph layout using qubit_coordinates, and now the rendered figures are much more similar to the original ones, (albeit rotated at a 90º angle).
One issue that may need to be addressed is using a better function than spring_layout for graph layouts. A grid_layout function can be developed for rustworkx, much like networkx. Should I open an issue for it? As of now, in the test file, if we increase the similarity tolerance of images in the test_plot_circuit_layout function from 0.1 to 0.2 (like the rest of the test functions), the tests will pass.

This is the core issue of #9031 that the algorithmic layout functions in rustworkx don't scale for large graphs. This is why I used graphviz_draw in the issue as graphviz will be able to layout the graph effectively no matter how large it is. I wasn't too concerned by the format of the visualization changing slightly as a result of using graphviz_draw. But if you'd prefer to use mpl_draw or alternatively the legacy manual mpl code what I'd recommend is doing what I described the other day is to use graphviz solely as a layout engine (#10208 (comment) ). Then we can leave the rendering to mpl and take advantage of graphviz's better layout.

My concern with just using mpl_draw though is rx.spring_layout (or any of the other layout methods available in rustworkx) doesn't scale. We do have an issue to track adding new layout methods: Qiskit/rustworkx#438 so if you're interested we can add a grid layout (I can also help you get started working on the implementation if you're interested in it). But, by itself having a grid layout method in rustworkx isn't really a general solution for #9031 because it assumes a grid like lattice connectivity. For example, if you had a 1000 qubit ring layout a grid layout wouldn't be a good layout.

I think the best way to test this issue is to build a couple of large qubit count fake backends and test them to see what the graphs look like. For example using main if you run:

import numpy as np
import rustworkx as rx

from qiskit import transpile, QuantumCircuit
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target, InstructionProperties
from qiskit.circuit.library import XGate, SXGate, RZGate, CZGate
from qiskit.circuit import Measure, Delay, Parameter, IfElseOp

from qiskit.visualization import plot_gate_map

class FakeMultiChip(BackendV2):
    """Fake multi chip backend."""

    def __init__(self, distance=3, number_of_chips=3):
        """Instantiate a new fake multi chip backend.

        Args:
            distance (int): The heavy hex code distance to use for each chips'
                coupling map. This number **must** be odd. The distance relates
                to the number of qubits by:
                :math:`n = \\frac{5d^2 - 2d - 1}{2}` where :math:`n` is the
                number of qubits and :math:`d` is the ``distance``
            number_of_chips (int): The number of chips to have in the multichip backend
                each chip will be a heavy hex graph of ``distance`` code distance.
        """
        super().__init__(name='multi_chip')
        graph = rx.generators.directed_heavy_hex_graph(distance, bidirectional=False)
        num_qubits = len(graph) * number_of_chips
        rng = np.random.default_rng(seed=12345678942)
        rz_props = {}
        x_props = {}
        sx_props = {}
        measure_props = {}
        delay_props = {}
        self._target = Target("Fake multi-chip backend", num_qubits=num_qubits)
        for i in range(num_qubits):
            qarg = (i,)
            rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
            x_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            sx_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            measure_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-3, 1e-1), duration=rng.uniform(1e-8, 9e-7)
            )
            delay_props[qarg] = None
        self._target.add_instruction(XGate(), x_props)
        self._target.add_instruction(SXGate(), sx_props)
        self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
        self._target.add_instruction(Measure(), measure_props)
        self._target.add_instruction(Delay(Parameter("t")), delay_props)
        cz_props = {}
        for i in range(number_of_chips):
            for root_edge in graph.edge_list():
                offset = i * len(graph)
                edge = (root_edge[0] + offset, root_edge[1] + offset)
                cz_props[edge] = InstructionProperties(
                    error=rng.uniform(1e-5, 5e-3), duration=rng.uniform(1e-8, 9e-7)
                )
        self._target.add_instruction(CZGate(), cz_props)
        self._target.add_instruction(IfElseOp, name="if_else")

    @property
    def target(self):
        return self._target

    @property
    def max_circuits(self):
        return None

    @classmethod
    def _default_options(cls):
        return Options(shots=1024)

    def run(self, circuit, **kwargs):
        raise NotImplementedError("Lasciate ogne speranza, voi ch'intrate")


class FakeGiantRing(BackendV2):
    """Fake multi chip backend."""

    def __init__(self):
        """Instantiate a new 1k qubit ring backend."""
        super().__init__(name='multi_chip')
        graph = rx.generators.directed_cycle_graph(1000)
        num_qubits = len(graph)
        rng = np.random.default_rng(seed=12345678942)
        rz_props = {}
        x_props = {}
        sx_props = {}
        measure_props = {}
        delay_props = {}
        self._target = Target("Fake multi-chip backend", num_qubits=num_qubits)
        for i in range(num_qubits):
            qarg = (i,)
            rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0)
            x_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            sx_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7)
            )
            measure_props[qarg] = InstructionProperties(
                error=rng.uniform(1e-3, 1e-1), duration=rng.uniform(1e-8, 9e-7)
            )
            delay_props[qarg] = None
        self._target.add_instruction(XGate(), x_props)
        self._target.add_instruction(SXGate(), sx_props)
        self._target.add_instruction(RZGate(Parameter("theta")), rz_props)
        self._target.add_instruction(Measure(), measure_props)
        self._target.add_instruction(Delay(Parameter("t")), delay_props)
        cz_props = {}
        for root_edge in graph.edge_list():
            edge = (root_edge[0], root_edge[1])
            cz_props[edge] = InstructionProperties(
                error=rng.uniform(1e-5, 5e-3), duration=rng.uniform(1e-8, 9e-7)
            )
        self._target.add_instruction(CZGate(), cz_props)
        self._target.add_instruction(IfElseOp, name="if_else")

    @property
    def target(self):
        return self._target

    @property
    def max_circuits(self):
        return None

    @classmethod
    def _default_options(cls):
        return Options(shots=1024)

    def run(self, circuit, **kwargs):
        raise NotImplementedError("Lasciate ogne speranza, voi ch'intrate")


if __name__ == "__main__":
    backend_multichip = FakeMultiChip(15, 5)
    backend_ring = FakeGiantRing()
    plot_gate_map(backend_multichip).savefig('giant_multichip.png')
    plot_gate_map(backend_ring).savefig('ring.png')

this is what you get out today:

giant_multichip.png: giant_multichip

ring.png: ring

which is less than useful :) While this output is with the current main branch, I expect this PR's branch using mpl_draw() to have similar output because it's relying on the same layout function under the covers. That script might be helpful in testing any potential solution to see how the proposed change performs on larger scale backends.

In that case, should I drop the qubit_coordinates parameter? If yes, the only remaining changes will be converting the PIL images of ghraphviz_draw() to matplotlib images and use the previous color schemes. Also, in that case, the color hex values will need to be reconverted to PyStrings.

@maxwell04-wq
Copy link
Contributor Author

maxwell04-wq commented Jul 20, 2023

I fixed the bug with the 1 qubit backend drawing (the CouplingMap(coupling).graph method to get the rustworkx graph wouldn't work because there are no edges in a 1 qubit graph). But I think I'm going to remove this from the 0.25 milestone in the interest of time since we're tagging 0.25.0rc1 tomorrow. The only thing really left is the image comparison tests, most of those will just need an update since the images aren't going to be identical anymore. But there are some weird things going on with the font scaling in some cases. For example, with this PR one of the visual tests is generating

16_qubit_gate_map

and the reference image is:

16_qubit_gate_map

besides being mirrored and different edges and node sizes (all of which I think are fine) the issue is the font is getting clipped by the node's circle. I think we should fix this before merging.

I've tested the new font size on a test code. Does this work?
(The unflattened edges are because I tested the code on an older remote codebase. Should not affect the recent changes.)

image

@mtreinish
Copy link
Member

Yeah, I think that should be fine, it's about legibility, not necessarily matching the exact image as before. As long as the numbers are legible and not getting clipped by the node's circle it should be fine.

@jakelishman
Copy link
Member

@maxwell04-wq: just so you know, you don't need to keep your branch up to date with main. We'll handle that automatically as part of the merge process.

@coveralls
Copy link

Pull Request Test Coverage Report for Build 6266203618

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • 277 unchanged lines in 5 files lost coverage.
  • Overall coverage decreased (-0.3%) to 87.006%

Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 93.76%
qiskit/extensions/quantum_initializer/squ.py 2 80.0%
crates/qasm2/src/lex.rs 4 91.41%
crates/qasm2/src/parse.rs 6 97.6%
qiskit/visualization/gate_map.py 264 7.0%
Totals Coverage Status
Change from base Build 6264759053: -0.3%
Covered Lines: 74066
Relevant Lines: 85127

💛 - Coveralls

Copy link
Member

@mtreinish mtreinish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just made some small tweaks to the formatting and fixed the font size issues. Also I updated the reference images in the visual comparison tests. I think this is ready to go now. Thanks a whole lot for doing this and sticking with it through all the updates. This project turned out to be a bit more finicky than it first seemed.

I think we'll probably be dealing with small formatting details for a while after making this change, but the gains from using a proper graph visualization engine are going to be a net win.

@mtreinish mtreinish added this pull request to the merge queue Sep 22, 2023
Merged via the queue into Qiskit:main with commit 778acaf Sep 22, 2023
14 checks passed
@mtreinish mtreinish added the Changelog: API Change Include in the "Changed" section of the changelog label Sep 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: API Change Include in the "Changed" section of the changelog Changelog: New Feature Include in the "Added" section of the changelog Community PR PRs from contributors that are not 'members' of the Qiskit repo mod: visualization qiskit.visualization
Projects
Status: Done
6 participants