Built-in Qubricks in Workbench¶
In this tutorial, we will learn about miscellaneous built-in Qubricks provided by Workbench.
Earlier, in the tutorial on quantum arithmetic, we went through quite a few built-in Qubricks that implement common arithmetic operations, bitwise operations, and comparisons. In this tutorial, we will continue the discussion by covering more built-in Qubricks Workbench offers for more advanced mathematical computations and commonly used tools such as QFT, reflection, and state preparation.
The Qubricks listed here are not an exhaustive list of libraries offered by Workbench! You can find more Qubricks in Workbench Algorithms ⧉.
Reflect¶
Reflection is the operation of selecting one basis state and applying a relative phase to it, leaving other basis states unchanged. This operation implements reflection about a state, hence the name. This operation is implemented as the Reflect Qubrick. It is also available as the reflect method of the Qubits register (see Basic Gates tutorial); in fact, that method calls the Reflect Qubrick under the hood.
The following example shows how to implement the first reflect example from the Basic Gates tutorial using a Qubrick instead of the reflect method.
from psiqworkbench import QPU, Qubits
from psiqworkbench.qubricks import Reflect
qpu = QPU(num_qubits=3)
reg = Qubits(3, "reg", qpu)
reg.had()
refl = Reflect()
# Multiply |111⟩ by -1
refl.compute(reg)
# Multiply |000⟩ by -1
refl.compute(~reg)
# Multiply |100⟩ by i
refl.compute(reg[0] | ~reg[1:], theta=90)
# Multiply |110⟩ by e^(iπ/4)
refl.compute(reg==3, theta=(1, 4))
qpu.print_state_vector()
qpu.draw(show_qubricks=True)
|reg> |0> -0.353553-0.000000j |1> 0.000000+0.353553j |2> 0.353553+0.000000j |3> 0.250000+0.250000j |4> 0.353553+0.000000j |5> 0.353553+0.000000j |6> 0.353553+0.000000j |7> -0.353553-0.000000j
Uniform state preparation¶
Uniform state preparation is the task of transforming a Qubits register in the $|0\rangle$ state into an equal superposition of the first $d$ basis states:
$$|0\rangle \rightarrow \sum_{k=0}^{d-1} \frac{1}{\sqrt{d}}|k\rangle$$
Workbench offers several built-in Qubricks that implement different algorithms for uniform state preparation (see Uniform State Preparation Qubricks). The following code snippet shows how to use two of them: USP, which allocates additional qubits to prepare the state, and ZAUSP, which prepares the state using zero auxiliary qubits (thus the name).
from psiqworkbench import QPU, Qubits
import psiqworkbench.qubricks as qbk
qpu = QPU(num_qubits=4)
reg = Qubits(2, "reg", qpu)
# Prepare a uniform state on already allocated register
qbk.USP().compute(3, reg)
# Allocate a new register and prepare a uniform state on it
qbk.ZAUSP(qc=qpu).compute(3)
qpu.print_state_vector()
qpu.draw(show_qubricks=True)
|reg|qbk_usp> |0|0> -0.192450+0.272166j |0|1> -0.192450+0.272166j |0|2> -0.192450+0.272166j |1|0> -0.192450+0.272166j |1|1> -0.192450+0.272166j |1|2> -0.192450+0.272166j |2|0> -0.192450+0.272166j |2|1> -0.192450+0.272166j |2|2> -0.192450+0.272166j
Quantum Fourier transform¶
Quantum Fourier transform is an extremely common routine in quantum algorithms. In Workbench, it is implemented using the QFT Qubrick. As usual in Workbench, QFT uses little-endian notation for converting basis states to integers used in QFT formulas.
The following code snippet shows how to use QFT to convert the basis state $|010\rangle$ to the following superposition state with periodic relative phase:
$$\tfrac1{2\sqrt2}\sum_{k=0}^{7} e^{i\pi \cdot k/2} |k\rangle =$$ $$\tfrac1{2\sqrt2}(|000\rangle + i|100\rangle - |010\rangle -i|110\rangle + |001\rangle + i|101\rangle - |011\rangle -i|111\rangle)$$
from psiqworkbench import QPU, Qubits
from psiqworkbench.qubricks import QFT
qpu = QPU(num_qubits=3)
reg = Qubits(3, "reg", qpu)
reg[1].x()
QFT().compute(reg)
qpu.print_state_vector()
qpu.draw(show_qubricks=True)
|reg> |0> 0.353553+0.000000j |1> 0.000000+0.353553j |2> -0.353553+0.000000j |3> -0.000000-0.353553j |4> 0.353553+0.000000j |5> 0.000000+0.353553j |6> -0.353553+0.000000j |7> -0.000000-0.353553j
QFT-based arithmetic¶
Workbench offers several Qubricks that implement QFT-based arithmetic: increment by a constant, addition, subtraction and multiplication. Unlike other built-in arithmetic Qubricks, these Qubricks only support QUInt data type. Increment, addition, and subtraction are done in-place, while multiplication allocates a new register to store and return its result.
The following example shows how to use QFT-based arithmetic Qubricks.
Notice that QFT addition is not modular; it will handle overflow by extending the target register. In the example below, the sum of
reg1andreg2is greater than the maximal number that can be stored in the two-qubit registerreg1, so the result register has three qubits:reg1and the extra qubittmpthat stores the most significant bit of the sum.
from psiqworkbench import QPU, Qubits
from psiqworkbench.qubricks import QFTIncrement, QFTAdd
qpu = QPU(num_qubits=5)
reg1 = Qubits(2, "reg1", qpu)
QFTIncrement().compute(reg1, 3)
qpu.print_state_vector()
reg2 = Qubits(2, "reg2", qpu)
reg2[1].x()
qpu.print_state_vector()
adder = QFTAdd()
adder.compute(reg1, reg2)
res = adder.get_result_qreg()
qpu.print_state_vector()
print(res.read())
qpu.draw(show_qubricks=True)
|reg1|?> |3|.> 1.000000-0.000000j |reg1|reg2|?> |3|2|.> 1.000000-0.000000j |reg1|reg2|tmp> |1|2|1> 1.000000-0.000000j 5
Square root¶
The Qubrick Sqrt implements the square root function. It takes a QUInt input and calculates two result values:
- The integer part of the square root $\lfloor \sqrt{x} \rfloor$ is returned as the "default" result of the computation and can be retrieved using
get_result_qreg()call without result name. - The remainder (the value $x - \lfloor \sqrt{x} \rfloor^2$) can be retrieved using
get_result_qreg("remainder").
The following example shows how to use the Sqrt Qubrick to compute the square root and the remainder.
from math import sqrt
from psiqworkbench import QPU, QUInt
from psiqworkbench.qubricks import Sqrt
qpu = QPU()
for val in range(16):
qpu.reset(19)
reg = QUInt(4, "reg", qpu)
reg.write(val)
qbk_sqrt = Sqrt()
qbk_sqrt.compute(reg)
res_main = qbk_sqrt.get_result_qreg()
res_rem = qbk_sqrt.get_result_qreg("remainder")
if val == 0:
qpu.draw(show_qubricks=True)
print("| x | sqrt(x) | Sqrt | Remainder |")
print(f"| {str(val).rjust(3)} | {sqrt(val):.3f} | {res_main.read()} | {res_rem.read()} |")
| x | sqrt(x) | Sqrt | Remainder | | 0 | 0.000 | 0 | 0 | | 1 | 1.000 | 1 | 0 | | 2 | 1.414 | 1 | 1 | | 3 | 1.732 | 1 | 2 | | 4 | 2.000 | 2 | 0 | | 5 | 2.236 | 2 | 1 | | 6 | 2.449 | 2 | 2 | | 7 | 2.646 | 2 | 3 | | 8 | 2.828 | 2 | 4 | | 9 | 3.000 | 3 | 0 | | 10 | 3.162 | 3 | 1 | | 11 | 3.317 | 3 | 2 | | 12 | 3.464 | 3 | 3 |
| 13 | 3.606 | 3 | 4 | | 14 | 3.742 | 3 | 5 | | 15 | 3.873 | 3 | 6 |
Reciprocal square root¶
Reciprocal square root of a number $x$, also known as inverse square root, is the value $\frac1{\sqrt{x}}$. Workbench implementation of this function is the RSqrt Qubrick which takes a QUInt register as the input and produces a QUFixed result. Same as the Sqrt Qubrick, this Qubrick produces two results:
- The low-precision result is computed in-place, with the Qubrick re-typing the input register as
QUFixedand marking it as the "default" result of the computation (the one you can retrieve usingget_result_qreg()call without result name). - The high-precision result reuses the same register as its most significant part and appends an auxiliary register to store the least significant digits; you can retrieve it using
get_result_qreg("high_precision").
The following example shows how to calculate reciprocal square roots of numbers from $1$ to $15$ and retrieve the results with low and high precision, comparing them to classically computed values.
from math import sqrt
from psiqworkbench import QPU, QUInt
from psiqworkbench.qubricks import RSqrt
qpu = QPU()
for val in range(1, 16):
qpu.reset(11)
reg = QUInt(4, "reg", qpu)
reg.write(val)
rsqrt = RSqrt()
rsqrt.compute(reg)
res_low = rsqrt.get_result_qreg()
res_high = rsqrt.get_result_qreg("high_precision")
if val == 1:
qpu.draw(show_qubricks=True)
print("| x | 1/sqrt(x) | RSqrt(low) | RSqrt(high) |")
print(f"| {str(val).rjust(3)} | {1 / sqrt(val):.7f} | {str(res_low.read()).ljust(8)} | {str(res_high.read()).ljust(9)} |")
| x | 1/sqrt(x) | RSqrt(low) | RSqrt(high) | | 1 | 1.0000000 | 0.9375 | 0.9375 | | 2 | 0.7071068 | 0.6875 | 0.6875 | | 3 | 0.5773503 | 0.5625 | 0.5625 | | 4 | 0.5000000 | 0.4375 | 0.46875 | | 5 | 0.4472136 | 0.4375 | 0.4375 | | 6 | 0.4082483 | 0.375 | 0.40625 | | 7 | 0.3779645 | 0.375 | 0.375 | | 8 | 0.3535534 | 0.3125 | 0.34375 | | 9 | 0.3333333 | 0.3125 | 0.34375 | | 10 | 0.3162278 | 0.3125 | 0.3125 | | 11 | 0.3015113 | 0.3125 | 0.3125 | | 12 | 0.2886751 | 0.25 | 0.28125 | | 13 | 0.2773501 | 0.25 | 0.28125 | | 14 | 0.2672612 | 0.25 | 0.25 | | 15 | 0.2581989 | 0.25 | 0.25 |
Applying a unitary described as a matrix¶
On a real quantum device, the only way to apply a unitary to a quantum register is through applying a sequence of gates. When running on a simulator, though, you can apply a unitary to the quantum state directly, by multiplying it by the corresponding matrix. (This is similar to how you can set the quantum state directly using the push_state method of the Qubits register.) To do this, you can use the Matrix Qubrick.
The following example demonstrates the use of the Matrix Qubrick. It defines a single-qubit matrix similar to that of the $Ry$ gate (up to the relative phase of the second column) and a two-qubit matrix that describes its controlled variant, and then applies these matrices to a single-qubit and a two-qubit register, respectively.
from numpy import array
from psiqworkbench import QPU, Qubits
from psiqworkbench.qubricks import Matrix
qpu = QPU(num_qubits=2)
reg = Qubits(2, "reg", qpu)
reg.push_state([0.6, 0, 0, 0.8])
qpu.print_state_vector()
m = array([[0.6, 0.8],
[0.8, -0.6]])
apply_matrix = Matrix()
# Apply the matrix to the least significant bit
apply_matrix.compute(m, reg[0])
qpu.print_state_vector()
# Shape of the same matrix controlled on the most significant bit
controlled_m = array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0.6, 0.8],
[0, 0, 0.8, -0.6]])
apply_matrix.compute(controlled_m, reg)
_ = qpu.print_state_vector()
|reg> |0> 0.600000+0.000000j |3> 0.800000+0.000000j |reg> |0> 0.360000+0.000000j |1> 0.480000+0.000000j |2> 0.640000+0.000000j |3> -0.480000+0.000000j |reg> |0> 0.360000+0.000000j |1> 0.480000+0.000000j |3> 0.800000+0.000000j
Next steps¶
In this tutorial, you've learned about additional built-in Qubricks in Workbench. Remember, you can access more library Qubricks via Workbench Algorithms ⧉!