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!
Op
andSub
- First, we will explain
Op
andSub
, which represent arbitrary quantum operations in Qsub.
- First, we will explain
- Standard predefined
Op
s:qsub.lib.std
- Next, we will introduce predefined
Op
s, which is provided in the standard library.
- Next, we will introduce predefined
- Resolving
Op
toSub
- Compile and analyze a
Sub
- The
Sub
thus 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
Op
andSub
- Users can define their own
Op
andSub
, which can be used in the same way as predefinedOp
andSub
. The last section describes how to create such customOp
andSub
.
- 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 Op
s.
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 Op
s 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 Op
s: qsub.lib.std
qsub.lib.std
package contains the following standard pre-defined Op
s.
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 Op
s 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.