Visualisation of circuits

Quantum computing basics part 2

A picture is worth a thousand words. This old saying holds true in almost any subject one can think of. It is no different when it comes to implementation of quantum circuits. Visualisation of circuits can be crucial, especially when you build bigger, more complicated ones. It may be much easier to understand what they do, and why they yield these (and not other) results when looking at their diagrams instead of the code that brought them to life. In this post I will walk you through the most common use cases in circuit visualisation.

I use Qiskit as the quantum framework of choice, and the code you see here is written in Python.

A simple circuit

You cannot visualise something that does not exist, so let us start with a rather simple circuit that we are going to use through most of this article. 

circuit = QuantumCircuit(3, 3)
circuit.h(range(3))
circuit.x(0)
circuit.cz(0, 1)
circuit.x(2)
circuit.measure(range(3), range(3))

Simple draw

So, what does this circuit do? Looking at the code, you can tell that it applies a Hadamard gate to all the qubits, then a NOT gate to the first qubit, a control-Z gate to the first and second qubits, and finally a NOT gate to the last qubit. Then a measurement is performed on all the qubits. But in order to tell it, you must remember how the range() function works, and how the qubits are ordered in Qiskit. It would be much easier to not have to rely on all this knowledge, would it not? 

Qiskit has a lot of sophisticated functionality as far as drawing the circuits goes, but it is also vital to know the basics. After all, all that functionality relies on packages and software that may not be available to you all the time. The first and simplest way to get a circuit print is to pass it to the print() function.

print(circuit)

It is not pretty, but it works. A slightly better result can be achieved by calling the draw() function on the circuit object.

This one looks much better. But can we even get better than this?

Rendering engines

Qiskit supports external rendering engines. A rendering engine is a software component responsible for converting data (like vector graphics, text, or 3D models) into a visual output (e.g., images, animations, or interactive displays). It handles tasks like drawing shapes, applying colors, managing fonts, and rendering layouts. You can use those engines to get better circuit diagrams than the simple draw allows for.

The less popular, but more capable, engine is LaTeX. It is a separate software package that must be installed independently on the system. If you use a MacBook like me, you can get a copy here. Additionally, you must ensure that your project uses the qcircuit package. Then you can select the LaTeX backend as your rendering system this way:

circuit.draw(output="latex")

The most popular engine is Matplotlib. I guess it is because its capabilities are fairly decent, but it is much easier to install, and manage. You just have to install the pylatexenc and matplotlib packages in your virtual environment (or Jupyter server), and you are good to go. Then you can print the circuit using the following command.

circuit.draw(output="mpl")

This one looks much better, does it not?

I will use Matplotlib in the rest of the examples in this article, but if you wish to use LaTeX instead, just change the value that you pass to the output parameter.

How does the  draw function work?

Normally, the  draw method returns the rendered image as an object and prints nothing.

Which class it returns depends on the output you specify: the default  ’text’  yields a  TextDrawer,  ’mpl’  yields a  matplotlib.Figure, and  ’latext’  yields a PIL.Image object. Jupyter notebooks recognise these return types and display them automatically, but outside of Jupyter the images will not appear on their own. So do not be surprised that nothing is drawn if you use a vanilla Python environment.

Saving the output

So far so good, but apart from visual inspection, the figures that we have plotted are not for much use. Sure, you can make print screens of them to include them in your documentation, or share around, but it will soon become cumbersome, or not really feasible (especially, if your circuits get bigger and bigger). Luckily, you can save the render to a separate file.

Sometimes our circuits can get quite big. Jupyter does its best to fit them in the available space, but it can be useful to save the diagram as an image file. The draw method has an optional argument filename that allows us to specify the name of the file where to save the diagram. The file format is inferred from the extension of the filename, and it can be any format supported by Matplotlib, such as PNG, PDF, SVG, etc. This way you can save the diagram in a high-quality format that can be easily included in reports, presentations, or publications, or just enlarge it to see the details more clearly.

circuit.draw(output="mpl", filename="circuit-mpl.png")

By default the file is saved to the current working directory, but you can provide a relative or an absolute path if you wish to override this behaviour.

circuit.draw(output="mpl", filename="./img/circuit-mpl.png")

Additional controls

Interactivity

Interactivity is actually a big word for what is available in circuit plotting. When you call the  draw function using either Matplotlib or LaTeX as the render engines, you can pass the interactivity=True argument in the  kwarg parameter. It will cause the drawing to be opened in a new window. It has one downside though. It does not work in Jupyter notebooks so well.

Barriers 

Barriers are a very useful tool in bigger circuits, but they can be effectively demonstrated in our small circuit too. Let us rewrite it adding two barriers to it.

circuit = QuantumCircuit(3, 3)
circuit.h(range(3))
circuit.barrier()
circuit.x(0)
circuit.cz(0, 1)
circuit.x(2)
circuit.barrier()
circuit.measure(range(3), range(3))

Calling the circuit.draw(output="mpl") function will give us the following result.

The barriers are those two vertical, grey bars. They do not have any logical effect on the circle, but allow us to visually group distinct parts of it.

Some circuits come in with built-in barriers. This can be the case when you use a quantum primitive, or you append sub-circuits to a bigger one. They are there to distinguish distinct parts of the super-circuit. But sometimes they may be in the way of what you are trying to achieve. You can disable them with the following line of code.

circuit.draw(output="mpl", plot_barriers=False)

In the case of our modified circuit we will get the following result.

Sure, in the case of our circuit it does not really make sense to disable the barriers that we put there ourselves, but, as I wrote, sometimes you do not control the code of the circuit, but would still like to get rid of the additional separators. 

Reversing the qubits

Qiskit uses little endian by default. If you do not know what I am writing about right now, please check a dedicated post on it that I published a while ago. This, however, may be in the way of how you want to visualise the data. You can easily reverse the order of the qubits in the render with this line. 

circuit.draw(output="mpl", reverse_bits=True)

As you can see, the qubits are listed now from the last (and not first) index at the top.

Styling

Surprisingly enough, Matplotlib supports providing custom styles to the rendering engine. You can create a style object (using a syntax known from CSS), and then pass it to your drawing function like in the following example.

background = {"backgroundcolor": "lightblue"}
circuit.draw(output="mpl", style=background)

It will obviously change the background colour to light blue.

You can also steer the scale of the image by setting the scale parameter of the draw function, like in the following example:

circuit.draw(output="mpl", scale=0.3)

This may help visualising bigger circuits in Jupyter notebooks by shrinking them.

Folding

Folding was a very popular technique of managing the output of programs back in the days where command line interfaces were prevalent. You can still see it in action today if you use tools like AWS CLI. It serves the same purpose in Qiskit. If you use the text rendering engine, and you have strict display size limitations, you can fold the output to the next line like this:

circuit.draw(output="text", fold=30)

The result looks somewhat funny, with the double arrows indicating the folding line. I do not recall using it before doing the research for this blog post, but I hope this trick may save you one day.

Standalone function

Last but not least, Qiskit allows printing circuits without invoking functions on them. There is a standalone function circuit_drawer that effectively does the same as the draw function from the QuantumCircuit class. You have to import a separate package — qiskit.visualization — in order to benefit from it, but otherwise the usage is quite straightforward.

from qiskit.visualization import circuit_drawer
circuit_drawer(circuit, output="mpl")

The function supports all of the parameters and print options that I discussed in this article, and I encourage you to experiment a bit with it.

Conclusion

Visualisation is not a luxury, it is a necessity when you start to juggle more than a handful of qubits or when you need to communicate your ideas to others. In Qiskit the draw method is the gateway to a rich set of tools that let you switch between a quick text preview, a tidy Matplotlib image, or a high‑resolution LaTeX output with a single line of code. You can even open the figure in a separate window, drop in barriers to group logical blocks, reverse the qubit order to match your intuition, and customize colours, scale or background with a few dictionary entries. If you prefer a more imperative style, the circuit_drawer helper gives you the same flexibility without having to touch the circuit object itself.

Beyond the aesthetics, these visual aids help you spot mistakes, understand interference patterns, and explain the behaviour of your circuit in papers, slides or notebooks. The ability to fold long text outputs, to save diagrams in vector formats, and to toggle optional features like barriers means you can adapt the same code to a wide range of audiences and display constraints.

Circuit visualisation is just a beginning, a step that you should remember before submitting your circuit to run on a simulator or real hardware. But the journey does not end here. In the next part of this series we will take a look at visualising computation results.

Pauli operators

Quantum computing basics part 1

Learning quantum computing (which I abbreviate as QC in this series’ titles) can be, and often is, challenging. One of the common problems is finding resources that help learning the basics with some practical examples (and not only mathematical equations). This inspired me to start a series of articles that cover exactly this — quantum computing basics — with focus on coding them with Qiskit. You can find all the articles under the #qcbasics hashtag.

In this post I write about Pauli operators — one of the most important single-qubit operators available for quantum programmers. They are used for the most common qubit operations, they form measurement bases, and they are absolutely crucial for quantum error correcting codes, among other things. They are also simple enough to be a great starting point.

Defining Pauli operators

There are four Pauli operators: identity, bit-flip, bit-and-phase-flip, and phase-flip. They are represented with the following symbols: {"font":{"size":11,"family":"Arial","color":"#000000"},"aid":null,"backgroundColor":"#ffffff","id":"1","code":"$$\\mathbb{I}$$","type":"$$","backgroundColorModified":false,"ts":1772710460915,"cs":"PBtLbk5xx0EER9KhBoxrtg==","size":{"width":5,"height":10}}, X, Y, and Z. We will look at them in detail in a moment. But let me cover their common properties before.

They are all represented by 2 ⨉ 2 matrices. Such matrices are called square matrices. All Pauli operators are also unitary matrices. 

Unitary matrices

A unitary matrix is a matrix that is invertible, which means that its inverse (U-1) is equal to its conjugate transpose (U*). What does it mean? Let us take the X operator as an example.

On the left-hand side you can see X’s reverse, and on the right-hand side X’s conjugate transpose.

(If you do not know how to calculate those, you can use this calculator.)

Finally, a unitary matrix must satisfy the following condition as well — a product of the matrix itself and its conjugate transpose must be equal to the identity matrix.

Generally, all quantum operators must be unitary, since everything that happens in a quantum processor must be reversible. 

Hermitian matrices

All Pauli operators are also Hermitian matrices. It means that they are equal to their own conjugate transpose. Following the previous example with the X operator, let us take a look at the following example.

This one was easier, was it not?

Anti-commuting

Each Pauli operator anti-commutes with each other (except for the identity operator). Fancy wording, but what does it mean? If a mathematical operations anti-commutes it means that swapping the position of two arguments of an this operation gives a result which is the inverse of the result with unswapped arguments. In case of the Pauli operators it looks like the following:

Multiplication

The Pauli operators multiply as any other square matrix, but I listed the multiplication rules here in case you would need them.

{"backgroundColor":"#ffffff","backgroundColorModified":false,"type":"$$","code":"$$i$$","id":"7","font":{"size":11,"color":"#000000","family":"Arial"},"aid":null,"ts":1772712509469,"cs":"yTgQcoob8MkrhfE41BAedw==","size":{"width":4,"height":10}} stands for an imaginary part of a complex number

The identity operator

We covered all the properties of Pauli operators, but we have not taken a look at the operators themselves. Let us begin with the identity operator, the only operator that is actually trivial in this set as it does nothing to the state of a qubit. The matrix for this operator looks like the following.

I promised practical examples, and the time has come to code something. All the examples in this article follows the same template:

  1. Define a |1⟩ qubit.
  2. Define a quantum circuit with the demonstrated operator as a gate, and visualise it.
  3. Define a quantum circuit with the demonstrated operator as an operator, and visualise it.

The most common way of using this operator is as a gate, like in the following example.

qc = QuantumCircuit(1)
qc.id(0)

Qiskit, however, allows for more complicated usage of Pauli operators using a dedicated class, and circuit appending instead.

qc = QuantumCircuit(1)
i_operator = Pauli('I')
qc.append(i_operator, [0])

In any case, the circuit looks like below and yields |1⟩ as the result.

The bit-flip operator

The identity operator is indeed trivial, so let us switch to something that actually does something — the bit-flip, or X operator. It does something relatively simple, namely reverses whatever the value of a qubit is. It is characterised by the following matrix.

Here is how the operator looks like as a gate.

qc = QuantumCircuit(1)
qc.x(0)

And the corresponding operator part.

qc = QuantumCircuit(1)
x_operator = Pauli('X')
qc.append(x_operator, [0])

In both cases, the result of this operation is |0⟩, and the circuit looks like the following.

The phase-flip operator

The next operator — the bit-flip or Z operator — does the same thing to the phase as the X operator does to the bit value. It flips it. The trick is that it is not visible in the classical readout operation that one usually applies to see the results. The state is purely quantum, and cannot be directly translated. It is defined by the following matrix.

When coded as a gate, it looks like this.

qc = QuantumCircuit(1)
qc.z(0)

And here as an operator.

qc = QuantumCircuit(1)
z_operator = Pauli('Z')
qc.append(z_operator, [0])

In both cases again, the result is the same. It is |-⟩ (would be |+⟩ if the input was |0⟩). And in the diagram.

The bit-and-phase-flip operator

Last but not least, we have the bit-and-phase or Y Pauli operator. It is sort of a mixture of the X and Z operators, since it does both things. Remember, however, that only the bit-flip part can be measured classically, as the phase part is lost during a readout. Here is the matrix.

If we implement it as code, we do it in the following way.

qc = QuantumCircuit(1)
qc.y(0)

And similarly as an operator.

qc = QuantumCircuit(1)
y_operator = Pauli('Y')
qc.append(y_operator, [0])

The result we get is |-i⟩ (would be |i⟩ if the input was |0⟩). The diagram is as follows.

Operators in Qiskit

Gates in Qiskit are handy functions that allow one to create circuits quickly, but abstract away a lot of details and low-level functionality. Defining circuits with operators is much more verbose, but allows implementations which are more detailed and flexible. I will not dig into the details here, since it is a broad topic which deserves its own article (or a book, actually). However, below you can find a sample code snippet that will give you a glimpse of their capabilities.

# Multi-qubit Pauli (Z ⊗ X on two qubits): Z on qubit 0, X on qubit 1.
pauli_zx = Pauli('ZX')

# Convert to Operator for matrix math.
op_x = Operator(pauli_x)
print(op_x.data)

# Check commutation.
print(pauli_x.compose(pauli_z).commutes(pauli_z.compose(pauli_x)))

Summary

The Pauli operators are one of the most basic building blocks of quantum circuits, gadgets and algorithms. There are four of them: {"font":{"size":11,"family":"Arial","color":"#000000"},"aid":null,"backgroundColor":"#ffffff","id":"1","code":"$$\\mathbb{I}$$","type":"$$","backgroundColorModified":false,"ts":1772710460915,"cs":"PBtLbk5xx0EER9KhBoxrtg==","size":{"width":5,"height":10}}, X, Y, and Z, also called the identity, bit-flip, bit-and-phase-flip, and phase-flip operators. Each of them is represented with a square matrix that is both unitary and Hermitian.

The operators can be used on a single qubit in Qiskit either as gates, or operators. The gate representation allows us for faster implementation, and cleaner code, but the operator representation gives us power over the low-level implementation details, and more flexibility.

Overall, the Pauli operators are something that any quantum developer must be familiar with, as they are virtually in any quantum circuit we will ever build. Even if we do not use them explicitly, they are the foundation of modern-day error-correction codes that allow us to use qubits reliably.