Tutorial
Installation
OpenSquirrel is available through the Python Package Index (PyPI).
Accordingly, installation is as easy as ABC:
You can check if the package is installed by importing it:
Creating a circuit
OpenSquirrel's entrypoint is the Circuit
, which represents a quantum circuit.
You can create a circuit in two different ways:
- form a string written in cQASM, or;
- by using the
CircuitBuilder
in Python.
1. From a cQASM string
from opensquirrel import Circuit
qc = Circuit.from_string(
"""
version 3.0
// Initialise a circuit with two qubits and a bit
qubit[2] q
bit[2] b
// Create a Bell pair
H q[0]
CNOT q[0], q[1]
// Measure qubits
b = measure q
"""
)
print(qc)
version 3.0
qubit[2] q
bit[2] b
H q[0]
CNOT q[0], q[1]
b[0] = measure q[0]
b[1] = measure q[1]
2. Using the CircuitBuilder
For creation of a circuit through Python, the CircuitBuilder
can be used accordingly:
from opensquirrel import CircuitBuilder
from opensquirrel.ir import Qubit, Float
builder = CircuitBuilder(qubit_register_size=2)
builder.Ry(Qubit(0), Float(0.23)).CNOT(Qubit(0),Qubit(1))
qc = builder.to_circuit()
print(qc)
version 3.0
qubit[2] q
Ry(0.23) q[0]
CNOT q[0], q[1]
You can naturally use the functionalities available in Python to create your circuit:
builder = CircuitBuilder(qubit_register_size=10)
for i in range(0, 10, 2):
builder.H(Qubit(i))
qc = builder.to_circuit()
print(qc)
version 3.0
qubit[10] q
H q[0]
H q[2]
H q[4]
H q[6]
H q[8]
For instance, you can generate a quantum fourier transform (QFT) circuit as follows:
qubit_register_size = 5
builder = CircuitBuilder(qubit_register_size)
for i in range(qubit_register_size):
builder.H(Qubit(i))
for c in range(i + 1, qubit_register_size):
builder.CRk(Qubit(c), Qubit(i), Int(c-i+1))
qft = builder.to_circuit()
print(qft)
version 3.0
qubit[5] q
H q[0]
CRk(2) q[1], q[0]
CRk(3) q[2], q[0]
CRk(4) q[3], q[0]
CRk(5) q[4], q[0]
H q[1]
CRk(2) q[2], q[1]
CRk(3) q[3], q[1]
CRk(4) q[4], q[1]
H q[2]
CRk(2) q[3], q[2]
CRk(3) q[4], q[2]
H q[3]
CRk(2) q[4], q[3]
H q[4]
Strong types
As you can see, gates require strong types. For instance, you cannot do:
try:
Circuit.from_string(
"""
version 3.0
qubit[2] q
CNOT q[0], 3 // The CNOT expects a qubit as second argument.
"""
)
except Exception as e:
print(e)
Parsing error: failed to resolve overload for cnot with argument pack (qubit, int)
The issue is that the CNOT expects a qubit as second input argument where an integer has been provided.
The same holds for the CircuitBuilder
, i.e.,
it also throws an error if arguments are passed of an unexpected type:
TypeError: wrong argument type for instruction `CNOT`, got <class 'int'> but expected Qubit
Modifying a circuit
Merging single qubit gates
All single-qubit gates appearing in a circuit can be merged by applying merge_single_qubit_gates()
to the circuit.
Note that multi-qubit gates remain untouched and single-qubit gates are not merged across any multi-qubit gates.
The gate that results from the merger of single-qubit gates will, in general,
comprise an arbitrary rotation and, therefore, not be a known gate.
In OpenSquirrel an unrecognized gate is deemed anonymous.
When a circuit contains anonymous gates and is written to a cQASM string,
the semantic representation of the anonymous gate is exported.
Warning
The semantic representation of an anonymous gate is not compliant cQASM, meaning that a cQASM parser, e.g. libQASM, will not recognize it as a valid statement.
import math
builder = CircuitBuilder(1)
for _ in range(4):
builder.Rx(Qubit(0), Float(math.pi / 4))
qc = builder.to_circuit()
qc.merge_single_qubit_gates()
print(qc)
version 3.0
qubit[1] q
Anonymous gate: BlochSphereRotation(Qubit[0], axis=[1. 0. 0.], angle=3.14159, phase=0.0)
In the above example, OpenSquirrel has merged all the Rx gates together. Yet, for now, OpenSquirrel does not recognize that this results in a single Rx over the cumulated angle of the individual rotations. Moreover, it does not recognize that the result corresponds to the X gate (up to a global phase difference). At a later stage, we may want OpenSquirrel to recognize the resultant gate in the case it is part of the set of known gates.
The gate set is, however, not immutable. In the following section, we demonstrate how new gates can be defined and added to the default gate set.
Defining your own quantum gates
OpenSquirrel accepts any new gate and requires its definition in terms of a semantic.
Creating new gates is done using Python functions, decorators, and one of the following gate semantic classes:
BlochSphereRotation
, ControlledGate
, or MatrixGate
.
- The
BlochSphereRotation
class is used to define an arbitrary single qubit gate. It accepts a qubit, an axis, an angle, and a phase as arguments. Below is shown how the X-gate is defined in the default gate set of OpenSquirrel:
@named_gate
def x(q: Qubit) -> Gate:
return BlochSphereRotation(qubit=q, axis=(1, 0, 0), angle=math.pi, phase=math.pi / 2)
Notice the @named_gate
decorator.
This tells OpenSquirrel that the function defines a gate and that it should,
therefore, have all the nice properties OpenSquirrel expects of it.
- The
ControlledGate
class is used to define a multiple qubit gate that comprises a controlled operation. For instance, the CNOT gate is defined in the default gate set of OpenSquirrel as follows:
@named_gate
def cnot(control: Qubit, target: Qubit) -> Gate:
return ControlledGate(control, x(target))
- The
MatrixGate
class may be used to define a gate in the generic form of a matrix:
@named_gate
def swap(q1: Qubit, q2: Qubit) -> Gate:
return MatrixGate(
np.array(
[
[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
]
),
[q1, q2],
)
Note
User defined gates can only be used in when creating a circuit with the circuit builder. cQASM parsers will not recognize user defined gates, i.e., they cannot be used when creating a circuit through a cQASM string.
Gate decomposition
OpenSquirrel can decompose the gates of a quantum circuit, given a specific decomposition. OpenSquirrel offers several, so-called, decomposers out of the box, but users can also make their own decomposer and apply them to the circuit. Decompositions can be: 1. predefined, or; 2. inferred from the gate semantics.
1. Predefined decomposition
The first kind of decomposition is when you want to replace a particular gate in the circuit, like the CNOT gate, with a fixed list of gates. It is commonly known that CNOT can be decomposed as H-CZ-H. This decomposition is demonstrated below using a Python lambda function, which requires the same parameters as the gate that is decomposed:
from opensquirrel.default_gates import CNOT, H, CZ
qc = Circuit.from_string(
"""
version 3.0
qubit[3] q
X q[0:2] // Note that this notation is expanded in OpenSquirrel.
CNOT q[0], q[1]
Ry q[2], 6.78
"""
)
qc.replace(
CNOT,
lambda control, target:
[
H(target),
CZ(control, target),
H(target),
]
)
print(qc)
version 3.0
qubit[3] q
X q[0]
X q[1]
X q[2]
H q[1]
CZ q[0], q[1]
H q[1]
Ry(6.78) q[2]
OpenSquirrel will check whether the provided decomposition is correct. For instance, an exception is thrown if we forget the final Hadamard, or H gate, in our custom-made decomposition:
qc = Circuit.from_string(
"""
version 3.0
qubit[3] q
X q[0:2]
CNOT q[0], q[1]
Ry q[2], 6.78
"""
)
try:
qc.replace(
CNOT,
lambda control, target:
[
H(target),
CZ(control, target),
]
)
except Exception as e:
print(e)
replacement for gate CNOT does not preserve the quantum state
2. Inferred decomposition
OpenSquirrel has a variety inferred decomposition strategies. More in depth tutorials can be found in the decomposition example Jupyter notebook.
One of the most common single qubit decomposition techniques is the Z-Y-Z decomposition.
This technique decomposes a quantum gate into an Rz
, Ry
and Rz
gate in that order.
The decompositions are found in opensquirrel.decomposer
,
an example can be seen below where a Hadamard, Z, Y and Rx gate are all decomposed on a single qubit circuit.
from opensquirrel.decomposer.aba_decomposer import ZYZDecomposer
builder = CircuitBuilder(qubit_register_size=1)
builder.H(Qubit(0)).Z(Qubit(0)).Y(Qubit(0)).Rx(Qubit(0), Float(math.pi / 3))
qc = builder.to_circuit()
qc.decompose(decomposer=ZYZDecomposer())
print(qc)
version 3.0
qubit[1] q
Rz(3.1415927) q[0]
Ry(1.5707963) q[0]
Rz(3.1415927) q[0]
Ry(3.1415927) q[0]
Rz(1.5707963) q[0]
Ry(1.0471976) q[0]
Rz(-1.5707963) q[0]
Similarly, the decomposer can be used on individual gates.
from opensquirrel.decomposer.aba_decomposer import XZXDecomposer
from opensquirrel.default_gates import H
print(ZYZDecomposer().decompose(H(Qubit(0))))
[BlochSphereRotation(Qubit[0], axis=Axis[0. 0. 1.], angle=1.5707963267948966, phase=0.0),
BlochSphereRotation(Qubit[0], axis=Axis[0. 1. 0.], angle=1.5707963267948966, phase=0.0),
BlochSphereRotation(Qubit[0], axis=Axis[0. 0. 1.], angle=1.5707963267948966, phase=0.0)]
Exporting a circuit
As you have seen in the examples above, you can turn a circuit into a
cQASM string
by simply using the str
or __repr__
methods.
We are aiming to support the possibility to export to other languages as well,
e.g., a OpenQASM 3.0 string, and frameworks, e.g., a Qiskit quantum circuit.