Circuits

In this module, you will learn how to use the qBraid SDK to interface with quantum circuit objects accross various frontends. We will demonstrate how to use the transpiler to convert circuits between packages, and highlight a few other circuit-based convenience features.

Program Types

Supported frontend program types include Qiskit, Amazon Braket, Cirq, PyQuil, PyTKET, and OpenQASM:

>>> from qbraid import QPROGRAM_TYPES
>>> for k in QPROGRAM_TYPES:
...     print(k)
...
braket.circuits.circuit.Circuit
cirq.circuits.circuit.Circuit
qiskit.circuit.quantumcircuit.QuantumCircuit
pyquil.quil.Program
pytket._tket.circuit.Circuit
qasm2
qasm3

Circuit Wrapper

We’ll start with a simple qiskit circuit that creates the bell state.

from qiskit import QuantumCircuit

def bell():
    circuit = QuantumCircuit(2)
    circuit.h(0)
    circuit.cx(0,1)
    return circuit
>>> qiskit_circuit = bell()
>>> qiskit_circuit.draw()
     ┌───┐
q_0: ┤ H ├──■──
     └───┘┌─┴─┐
q_1: ─────┤ X ├
          └───┘

Next, we’ll apply the qbraid circuit wrapper.

from qbraid import circuit_wrapper

qprogram = circuit_wrapper(qiskit_circuit)

Each circuit wrapper object has num_qubits and depth attributes, regardless of the input circuit type. The underlying “wrapped” circuit can be accessed using the circuit wrapper’s program attribute.

>>> qprogram.num_qubits
2
>>> qprogram.depth
2
>>> type(qprogram.program)
qiskit.circuit.quantumcircuit.QuantumCircuit

Transpiler

Now, we can use the qbraid.transpiler.QuantumProgramWrapper.transpile method to convert to wrapped circuit into any other supported program type. Simply pass in the name of the target package from one of qbraid.QPROGRAM_LIBS. For example, use input "braket" to return a braket.circuits.Circuit:

>>> braket_circuit = qprogram.transpile("braket")
>>> print(braket_circuit)
T  : |0|1|

q0 : -H-C-
        |
q1 : ---X-

T  : |0|1|

This time, using the same origin circuit wrapper, we’ll input "pyquil" to return a pyquil.quil.Program:

>>> pyquil_program = qprogram.transpile("pyquil")
>>> print(pyquil_program)
H 0
CNOT 0 1

Interface

The qbraid.interface module contains a number of functions that can be helpful for testing, quick calculations, verification, or other general use.

Random circuits

The random_circuit function creates a random circuit of any supported frontend program type. Here, we’ve created a random cirq.Circuit with four qubits and depth four.

>>> from qbraid.interface import random_circuit
>>> cirq_circuit = random_circuit("cirq", num_qubits=4, depth=4)
>>> print(cirq_circuit)
      ┌──────┐   ┌──┐           ┌──┐
0: ────iSwap───────@────@───Z──────────
       │           │    │
1: ────┼──────────X┼────@───@────@─────
       │          ││        │    │
2: ────┼────Z─────┼@────────X────┼H────
       │          │              │
3: ────iSwap──────@─────H────────X─────
      └──────┘   └──┘           └──┘

Unitary calculations

The to_unitary method will calculate the matrix representation of an input circuit of any supported program type.

>>> from qbraid.interface import to_unitary
>>> cirq_unitary = to_unitary(cirq_circuit)
>>> cirq_unitary.shape
(16, 16)

We can now apply the circuit wrapper to the random Cirq circuit above, and use the transpiler to return the equivalent pyquil.Program:

>>> pyquil_circuit = circuit_wrapper(cirq_circuit).transpile("pyquil")
>>> print(pyquil_circuit)
ISWAP 0 3
Z 1
CNOT 0 2
CZ 3 1
CZ 2 3
H 0
Z 3
CNOT 2 1
CNOT 2 0
H 1

To verify the equivalence of the two circuits, we can use the circuits_allclose method. It applies to_unitary to both input circuits, compares the outputs via numpy.allclose, and returns the result.

>>> from qbraid.interface import circuits_allclose
>>> circuits_allclose(cirq_circuit, pyquil_circuit)
True

Qubit Indexing

As a tool for interfacing between frontend modules, the qBrad SDK has a number of methods and functions dedicated to resolving any potential compatibility issues. For instance, each frontend has slightly different rules and standard conventions when it comes to qubit indexing. Functions and/or methods in some modules require that circuits are constructed using contiguous qubits i.e. sequential qubit indexing, while others do not. The convert_to_contiguous method can be used to map qubit indicies accordingly, and address compatibility issues without re-constructing each circuit.

For example, let’s look at a Braket circuit that creates a GHZ state.

from braket.circuits import Circuit

def ghz():
    circuit = Circuit()
    circuit.h(0)
    circuit.cnot(0, 2)
    circuit.cnot(2, 4)
    return circuit

Notice, our three-qubit circuit uses qubit indicies [0,2,4]:

>>> braket_circuit = ghz()
>>> print(braket_circuit)
T  : |0|1|2|

q0 : -H-C---
        |
q2 : ---X-C-
          |
q4 : -----X-

T  : |0|1|2|

From here, we can use convert_to_contiguous to map the circuit to the [0,1,2] convention. If the use-case requires using the dimensionality of the maximally indexed qubit, you can set expansion=True to append identity gates to “vacant” registers instead of performing the qubit mapping.

>>> from qbraid.interface import convert_to_contiguous
>>> print(convert_to_contiguous(braket_circuit))
T  : |0|1|2|

q0 : -H-C---
        |
q1 : ---X-C-
          |
q2 : -----X-

T  : |0|1|2|
>>> print(convert_to_contiguous(braket_circuit, expansion=True))
T  : |0|1|2|

q0 : -H-C---
        |
q1 : -I-|---
        |
q2 : ---X-C-
          |
q3 : -I---|-
          |
q4 : -----X-

T  : |0|1|2|