Qsub quick overview
Introduction
This notebook will show you how to use Qsub to construct structured quantum programs that represents FTQC algorithms. Let's look at how to use Qsub, step by step, in the following order!
OpandSub- First, we will explain
OpandSub, which represent arbitrary quantum operations in Qsub.
- First, we will explain
- Standard predefined
Ops:qsub.lib.std- Next, we will introduce predefined
Ops, which is provided in the standard library.
- Next, we will introduce predefined
- Resolving
OptoSub - Compile and analyze a
Sub- The
Subthus created must be resolved and compiled in order to perform resource estimation and generate quantum circuits for QURI Parts. The following sections describe these resolve and compile processes.
- The
- Custom
OpandSub- Users can define their own
OpandSub, which can be used in the same way as predefinedOpandSub. The last section describes how to create such customOpandSub.
- Users can define their own
Op and Sub
In Sub a circuit is represented as a Sub (subroutine) object. A Sub is defined as a sequence of operations on qubits and classical registers.
The most basic thing you can do is to define a Sub using predefined Ops.
from quri_parts.qsub.sub import SubBuilder
# Predefined Ops
from quri_parts.qsub.lib.std import H, CNOT, RZ
# Build a circuit for 2 qubits
b = SubBuilder(2)
q0, q1 = b.qubits
b.add_op(H, (q1,))
b.add_op(CNOT, (q0, q1))
b.add_op(RZ(-0.5), (q1,))
b.add_op(CNOT, (q0, q1))
b.add_op(RZ(0.5), (q1,))
b.add_op(H, (q1,))
sub = b.build()
from quri_parts.qsub.visualize import draw_sub
draw_sub(sub)
Some Ops are parametric: in the above example, RZ is a parametric Op. It is just a function returning a (non-parametric) Op: RZ(0.5) is an Op with the fixed parameter 0.5.
Standard predefined Ops: qsub.lib.std
qsub.lib.std package contains the following standard pre-defined Ops.
from math import pi
from quri_parts.qsub.lib import std
print("Single qubit non-parametric gates:")
for op in [
std.X,
std.Y,
std.Z,
std.H,
std.SqrtX,
std.SqrtXdag,
std.SqrtY,
std.SqrtYdag,
std.S,
std.Sdag,
std.T,
std.Tdag,
]:
print(" ", op)
print("Single qubit parametric rotation gates:")
for op in [
std.RX,
std.RY,
std.RZ,
std.Phase,
]:
print(" ", op(pi/8))
print("Two qubit non-parametric gates:")
for op in [
std.CNOT,
std.CZ,
std.SWAP,
]:
print(" ", op)
print("Three qubit non-parametric gates:")
print(" ", std.Toffoli)
print("Measurement:")
print(" ", std.M)
print("Classical conditional branching:")
for op in [
std.Cbz,
std.Label,
]:
print(" ", op)
_ = [
"conditional",
"Inverse",
"Controlled",
"MultiControlled",
"scoped_and",
"scoped_and_clifford_t",
]
Single qubit non-parametric gates:
lib.std.X(qubits=1, registers=0)
lib.std.Y(qubits=1, registers=0)
lib.std.Z(qubits=1, registers=0)
lib.std.H(qubits=1, registers=0)
lib.std.SqrtX(qubits=1, registers=0)
lib.std.SqrtXdag(qubits=1, registers=0)
lib.std.SqrtY(qubits=1, registers=0)
lib.std.SqrtYdag(qubits=1, registers=0)
lib.std.S(qubits=1, registers=0)
lib.std.Sdag(qubits=1, registers=0)
lib.std.T(qubits=1, registers=0)
lib.std.Tdag(qubits=1, registers=0)
Single qubit parametric rotation gates:
lib.std.RX<0.39269908169872414>(qubits=1, registers=0)
lib.std.RY<0.39269908169872414>(qubits=1, registers=0)
lib.std.RZ<0.39269908169872414>(qubits=1, registers=0)
lib.std.Phase<0.39269908169872414>(qubits=1, registers=0)
Two qubit non-parametric gates:
lib.std.CNOT(qubits=2, registers=0)
lib.std.CZ(qubits=2, registers=0)
lib.std.SWAP(qubits=2, registers=0)
Three qubit non-parametric gates:
lib.std.Toffoli(qubits=3, registers=0)
Measurement:
lib.std.M(qubits=1, registers=1)
Classical conditional branching:
lib.std.Cbz(qubits=0, registers=2)
lib.std.Label(qubits=0, registers=1)
There are some parametric Ops that take another Op as their argument.
Inverse
Inverse returns inverse of a given unitary operation.
from quri_parts.qsub.lib import std
invS = std.Inverse(std.S)
print(invS)
lib.std.Inverse<lib.std.S>(qubits=1, registers=0)
Controlled
Controlled represents a controlled operation of a given unitary operation. If the given unitary is -qubit operation, then the returned controlled operation is -qubit operation; The first qubit is the control qubit.
from quri_parts.qsub.lib import std
ctrlS = std.Controlled(std.S)
print(ctrlS)
ctrlToffoli = std.Controlled(std.Toffoli)
print(ctrlToffoli)
lib.std.Controlled<lib.std.S>(qubits=2, registers=0)
lib.std.Controlled<lib.std.Toffoli>(qubits=4, registers=0)
MultiControlled
MultiControlled represents a multi-bit controlled operation of a given unitary operation. It takes three arguments: the target unitary Op, number of control bits (int) and control value (int). The control value is interpreted by binary representation: e.g. if control_value=0b10100, it is interpreted as bit0=0, bit1=0, bit2=1, bit3=0 and bit4=1.
from quri_parts.qsub.lib import std
mctrlS = std.MultiControlled(std.S, 3, 0b010)
print(mctrlS)
lib.std.MultiControlled<lib.std.S, 3, 2>(qubits=4, registers=0)
Resolving Op to Sub
An Op itself is just an abstract symbol. Eventually it needs to be resolved as either a primitive (native gate) or a Sub. How each Op should be resolved can be registered to a SubRepository, though users usually don't need to be aware of it since we provide a default SubRepository. In order to get a Sub corresponding to an Op, you can use resolve_sub() function. If no correspondence is registered for the given Op, it returns None:
from quri_parts.qsub.resolve import resolve_sub
from quri_parts.qsub.lib import std
subS = resolve_sub(std.S)
print(subS)
None
Most operations in qsub.lib.std are usually treated as primitives, so no correspondence for them is registered by default.