Skip to content

Circuit Diagrams from Q# Code

Mine Starks edited this page Apr 23, 2024 · 7 revisions

Circuit Diagrams from Q# Code

The QDK VS Code extension and qsharp Python package both provide the ability to create circuit diagrams from Q# code.

These diagrams capture the quantum operations (gates) that have been applied during the execution of the program.

VS Code

Circuit diagram for a Q# program

You can use the "Circuit" code lens to show a circuit diagram for a Q# program.

image

Clicking on this code lens will cause a panel titled "Q# Circuit" to open up.

image

Circuit diagrams for operations

In addition to showing the circuit for the whole program, you can also show the circuit for an individual operation that is declared in your code.

As long as the operation only takes parameters of type Qubit or Qubit[], a circuit can be generated for it.

In the editor, look for a Circuit code lens above the operation.

image

Clicking on this code lens will show the circuit diagram for only that operation.

image-1

Note that this operation takes an array of qubits (Qubit[]). Circuit generation will assume a length of 2 for qubit array arguments.

Circuit diagrams while debugging

You can also view circuit diagrams during step-by-step debugging of Q# programs.

Click the "Debug" code lens (or invoke the "Debug Q# File" command) to start stepping through the Q# source code.

image

In the Run and Debug view, expand "Quantum Circuit" under the Variables pane.

image

The current quantum circuit will be shown in the Q# Circuit pane.

image-2

This circuit diagram represents the current state of the simulator, i.e. the gates that have been applied up until the current point of execution.

Python

Refer to circuits.ipynb for a walkthrough of circuit diagrams in Python. (Note that the GitHub web viewer does not display the circuit images in this notebook. Open the notebook in VS Code or Jupyter to display the circuits).

Conditions that affect circuit diagrams

Dynamic circuits

Dynamic circuits are circuits that change based on the outcome of a measurement.

Circuit synthesis works by executing all the classical logic within a Q# program and tracing any qubits that have been allocated or gates that have been applied. Loops (e.g. for) and conditionals (if) are supported as long as they only deal with classical values.

However, programs that contain loops and conditional expressions that use qubit measurement results are trickier to represent with a circuit diagram. An expression like

H(q);
if (M(q) == One) {
   X(q)
}

cannot be represented with a straightforward circuit diagram, since the gates are conditional on a measurement result. Such a circuit is called a dynamic circuit.

Circuit diagrams can be generated for dynamic circuits by running the program in the quantum simulator, and tracing the gates as they are applied. This is called trace mode, as the qubits and gates are being traced as simulation is being performed.

The downside of traced circuits is that they only capture the measurement outcome, and the consequent gate applications, for a single simulation.

In the above example, if the measurement outcome is One, there is an X gate in the diagram.

image-5

Another run of the simulation may yield a measurement outcome of Zero, and now we don't see the X gate in the diagram.

image-6

Target profile

The currently selected target profile (set using qsharp.init() in Python, and the status bar icon in VS Code) influences how the circuit is synthesized.

Specifically, gate decompositions are applied that would make the resulting circuit compatible with the capabilities of the target hardware. These are the same decompositions that would get applied during code (QIR) generation and submission to Azure Quantum.

As an example, take the Measurement.qs sample. When target profile is set to "Unrestricted", the gates displayed on the circuit correspond exactly to the quantum operations that are invoked in the Q# program.

image

When the target profile is "QIR: Base", the circuit looks different.

image

Since Base Profile targets do not allow qubit reuse after measurement, the measurement is now performed on an entangled qubit instead. Since Reset is not a supported gate in Base Profile, it's dropped. The resulting circuit matches what would be run on hardware if this program was submitted to Azure Quantum with this target profile.

Troubleshooting

I'm getting different circuit diagrams for the same program.

  1. Target profile: Depending on the target chosen, the same program can synthesize to different circuits (see "Target profile" section).
  2. Result comparisons: If the program contains any conditionals that depend on measurement results, each run of the simulation may yield a different result (see "Dynamic circuits" section).
  3. Gate decomposition: When the target is set to "QIR: Base", synthesizing a circuit for the program ("Show Circuit" command) and showing the circuit for a simulator run ("Run file and show circuit") may yield different results. This is because gate decompositions are not applied when running under the simulator, to represent the behavior of the simulator. However during circuit synthesis these decompositions will be applied (see "Target profile" section).

I don't see the Circuit code lens for an operation in my code.

  1. Only operations that take Qubit or Qubit[] arguments show the "Circuit" code lens. If your operation takes no arguments, any classical arguments, or qubit tuple arguments, directly synthesizing a circuit for them is not supported.
  2. internal operations aren't supported (you can remove the internal keyword to enable the circuit).

Circuit synthesis for a specific operation fails

When an operation takes a Qubit[] argument, circuit synthesis assumes an array length of 2. It's possible that a specific operation makes assumptions about array length that simply doesn't work in this scenario. (e.g. Fact(Length(logicalQubit) == 3, "`logicalQubit` must be length 3");) Circuits can't be generated for such operations.

As an alternative, you can call this operation from your entry point with the appropriate array length, and synthesize a circuit for the whole program, or in Python, you can declare an operation that calls this one with the appropriate register width, and synthesize a circuit for that operation instead.